第十五章 流程事务的持久特性
在本章中,你将修改应用程序来配合同一数据库事务执行持久化的工作流更新数据库。这将能保证申请表中的实例数据是一致的。你将提供继承自PersistenceParticipant类的扩展并且在工作流被持久化时更新应用程序数据。
配置解决方案
你将使用第14章的项目继续开发,创建一个空白的解决方案如下图所示,并将解决方案命名为Chapter15:
Figure 15-1. Creating a blank solution
在window资源管理器下把Chapter14下的LeadGenerator文件夹复制到Chapter15下面。然后将LeadGenerator项目附加到Chapter15解决方案中。
然后创建名为Chapter15的数据库,并执行下面的脚本初始化数据库:
• SqlWorkflowInstanceStoreSchema.sql
• SqlWorkflowInstanceStoreLogic.sql
• Lead.sql
• Tracking.sql
打开app.config文件,将connectionStrings节点中的连接数据库Chapter14改为Chapter15。
PersistenceParticipant
在第12章中,你创建一个CommentExtension并允许添加一个注释字符串。注释随着继承自PersistenceParticipant类的实例数据被持久化到数据库中,同时也重写了CollectValues()和PublishValues()方法。在本章中你将使用同样的方法,但首先,让我们来回顾一下:
持久特性提供了两个基本操作:
• 保存—工作流实例持久化到存储设备
• 加载—流程实例加载到内存中
当执行了其中的一个操作时,流程实例遍历包含的所有扩展,并调用派生自PersistenceParticipant类的扩展中合适的方法。然后在PublishValues()方法调用后传递回CommentExtension。虽然这是将自定义数据持久化为流程的状态数据,但现在在本章中你将学习到一种技术来更新应用程序中的数据表(如Lead和Assignment表)。
PersistLead扩展
接下来你将创建一个自定义扩展以使流程被持久化时执行对Lead表的更新。在项目的Extensions 文件夹中新建名为PersistLead.cs的类,具体实现代码如下:
Listing 15-1. Implementation of PersistLead.cs
using System;
using System.Activities.Persistence;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq;
using System.Transactions;
using System.Xml.Linq;
namespace LeadGenerator.Extensions
{
public class PersistLead : PersistenceParticipant
{
private string _connectionString;
private IDictionary<Guid, Lead> _object;
private IDictionary<Guid, string> _action;
public PersistLead(string connectionString)
{
_connectionString = connectionString;
_object = new Dictionary<Guid, Lead>();
_action = new Dictionary<Guid, string>();
}
internal void AddLead(Lead l, string action)
{
_object.Remove(l.WorkflowID);
_action.Remove(l.WorkflowID);
_object.Add(l.WorkflowID, l);
_action.Add(l.WorkflowID, action);
}
protected override void CollectValues
(out IDictionary<XName, object> readWriteValues,
out IDictionary<XName, object> writeOnlyValues)
{
// We're not actually providing data to the caller
readWriteValues = null;
writeOnlyValues = null;
// See if there is any work to do...
if (_object.Count > 0)
{
// Get the current transaction
Transaction t = System.Transactions.Transaction.Current;
// Setup the DataContext
LeadDataDataContext dc = new LeadDataDataContext(_connectionString);
// Open the connection, if necessary
if (dc.Connection.State == System.Data.ConnectionState.Closed)
dc.Connection.Open();
if (t != null)
dc.Connection.EnlistTransaction(t);
// Process each object in our work queue
foreach (KeyValuePair<Guid, Lead> kvp in _object)
{
Lead l = kvp.Value as Lead;
string action = _action[l.WorkflowID];
// Perform the insert
if (action == "Insert")
{
dc.Lead.InsertOnSubmit(l);
}
// Perform the update
if (action == "Update")
{
dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Lead);
Lead lTmp = dc.Lead.SingleOrDefault<Lead>
(x => x.WorkflowID == l.WorkflowID);
if (lTmp != null)
{
lTmp.AssignedTo = l.AssignedTo;
lTmp.Status = l.Status;
}
}
}
// Submit all the changes to the database
dc.SubmitChanges();
// Remove all objects since the changes have been submitted
_object.Clear();
_action.Clear();
}
}
}
}
PersistLead类有两个Dictionary对象:第一个Lead对象存储了需要持久化的记录,第二个参数存储了请求的动作(Insert、Update、Delete),两个参数都用到了相同的流程实例ID。Dictionary集合中表示了工作队列中需要用到的数据。在应用程序或流程中需要用到AddLead()方法往工作队列中添加项目。
提示:你应当确保在AddLead()方法中首先调用remove()方法移除此实例中不再用到的项目。有一种典型的情况被修改和更新到数据的Lead记录还在工作队列中,所以,在对数据进行第二次更改操作时需要调用remove()方法之前缓存的实例。
连接数据库
SqlWorkflowInstanceStore类使用从从静态的事务Transaction类中获取到的当前事务,然后使用前几章使用实体对象关系设计器创建的DataContext类,并给其构造函数传递连接字符串打开数据库连接。最后,如果当前存在事务则通过DataContext调用EnlistTransaction()方法将其登记。
执行更新
CollectValues()方法遍历工作队列中的所有项以获得Lead对象和动作action。当action是Insert时,调用了DataContext中的InsertOnSubmit()方法提交数据。当action是Update时,首先查询数据库中的记录然后将工作队列中的Lead对象更新到数据库。在当前项目中,一旦Lead对象被插入到数据库,仅允许在分配代理人时修改Status和AssignedTo列。
在工作队列中的所有项目操作完成后将调用SubmitChanges()方法提交对所有数据的更改,这样的更改只有在所有的持久动作完成后才会被提交。最后一步是清除工作队列中的所有项,以防止下次持久化实例时还会进行相同的操作。
提示:实际上在持久化流程是不用返回任何形式的数据,readWriteValues和writeOnlyValues默认为null。注意,因为我们不期望取得任何数据,所以在这里并不需要去继承PublishValues()。
使用PersistLead扩展
接下来你需要去修改用到Lead表的地方用PersistLead扩展代替它,所以需要修改两个自定义的活动CreateLead和 AssignLead。
修改CreateLead活动
现在你需要修改CreateLead活动使用PersistLead扩展来将Lead记录插入到数据库。打开Activities文件夹中的CreateLead.cs文件。修改的实现如清单15-2所示:
Listing 15-2. Modified Implementation of CreateLead.cs
using System;
using System.Activities;
using System.Activities.Tracking;
using LeadGenerator.Extensions;
namespace LeadGenerator.Activities
{
/*****************************************************/
// This custom activity creates a Lead class using
// the input parameters (ContactName, ContactPhone,
// Interests and Notes). A Lead record is inserted
// into the database and then this is returned in
// the Lead output parameter.
/*****************************************************/
public sealed class CreateLead : CodeActivity
{
public InArgument<string> ContactName { get; set; }
public InArgument<string> ContactPhone { get; set; }
public InArgument<string> Interests { get; set; }
public InArgument<string> Notes { get; set; }
public InArgument<string> ConnectionString { get; set; }
public OutArgument<Lead> Lead { get; set; }
protected override void Execute(CodeActivityContext context)
{
// Create a Lead class and populate it with the input arguments
Lead l = new Lead();
l.ContactName = ContactName.Get(context);
l.ContactPhone = ContactPhone.Get(context);
l.Interests = Interests.Get(context);
l.Comments = Notes.Get(context);
l.WorkflowID = context.WorkflowInstanceId;
l.Status = "Open";
// Add this to the work queue to be persisted later
PersistLead persist = context.GetExtension<PersistLead>();
persist.AddLead(l, "Insert");
// Store the request in the OutArgument
Lead.Set(context, l);
// Add a custom track record
CustomTrackingRecord userRecord = new CustomTrackingRecord("New Lead")
{
Data =
{
{"Name", l.ContactName},
{"Phone", l.ContactPhone}
}
};
// Emit the custom tracking record
context.Track(userRecord);
}
}
}
本类中移除了之前执行数据库更新的代码并用下面的代码替换它:
// Add this to the work queue to be persisted later
PersistLead persist = context.GetExtension<PersistLead>();
persist.AddLead(l, "Insert");
这段代码使用了PersistLead扩展调用AddLead()方法来添加Lead对象到工作队列中以便实例被持久化时将数据插入数据库。
修改AssignLead活动
下面你需要对AssignLead活动做相似的修改,修改的代码实现如清单15-3所示:
Listing 15-3. Modified Implementation of AssignLead.cs
using System;
using System.Activities;
using LeadGenerator.Extensions;
namespace LeadGenerator.Activities
{
/*****************************************************/
// This custom activity assigns a Lead to the specified
// person (AssignedTo parameter). The updated Lead is
// returned in the output parameter.
/*****************************************************/
public sealed class AssignLead : CodeActivity
{
public InArgument<string> AssignedTo { get; set; }
public InOutArgument<Lead> Lead { get; set; }
protected override void Execute(CodeActivityContext context)
{
Lead l = Lead.Get(context);
l.AssignedTo = AssignedTo.Get(context);
l.Status = "Assigned";
PersistLead persist = context.GetExtension<PersistLead>();
persist.AddLead(l, "Update");
// Store the request in the OutArgument
Lead.Set(context, l);
}
}
}
PersistAssignment扩展
现在你需要提供与PersistLead相似的扩展来持久化Assignment表。在项目的Extensions文件夹中添加PersistAssignment.cs.文件,具体实现如清单15-4所示:
Listing 15-4. Implementation of PersistAssignment.cs
using System;
using System.Activities.Persistence;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq;
using System.Transactions;
using System.Xml.Linq;
namespace LeadGenerator.Extensions
{
public class PersistAssignment : PersistenceParticipant
{
private string _connectionString;
private IDictionary<Guid, Assignment> _object;
private IDictionary<Guid, string> _action;
public PersistAssignment(string connectionString)
{
_connectionString = connectionString;
_object = new Dictionary<Guid, Assignment>();
_action = new Dictionary<Guid, string>();
}
internal void AddAssignment(Guid id, Assignment a, string action)
{
// Make sure there isn't one already here
_object.Remove(id);
_action.Remove(id);
_object.Add(id, a);
_action.Add(id, action);
}
protected override void CollectValues
(out IDictionary<XName, object> readWriteValues,
out IDictionary<XName, object> writeOnlyValues)
{
// We're not actually providing data to the caller
readWriteValues = null;
writeOnlyValues = null;
// See if there is any work to do...
if (_object.Count > 0)
{
// Get the current transaction
Transaction t = System.Transactions.Transaction.Current;
// Setup the DataContext
LeadDataDataContext dc = new LeadDataDataContext(_connectionString);
// Open the connection, if necessary
if (dc.Connection.State == System.Data.ConnectionState.Closed)
dc.Connection.Open();
if (t != null)
dc.Connection.EnlistTransaction(t);
// Process each object in our work queue
foreach (KeyValuePair<Guid, Assignment> kvp in _object)
{
Assignment a = kvp.Value as Assignment;
string action = _action[kvp.Key];
// Perform the insert
if (action == "Insert")
{
dc.Assignment.InsertOnSubmit(a);
}
// Perform the update
if (action == "Update")
{
dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Lead);
Assignment aTmp = dc.Assignment
.SingleOrDefault<Assignment>
(x => x.WorkflowID == kvp.Key);
if (aTmp != null)
{
aTmp.DateCompleted = a.DateCompleted;
aTmp.Remarks = a.Remarks;
aTmp.Status = a.Status;
}
}
}
// Submit all the changes to the database
dc.SubmitChanges();
// Remove all objects since the changes have been submitted
_object.Clear();
_action.Clear();
}
}
}
}
这个类的通过AddAssignment()方法传递工作流实例ID,因为Assignment类没有像Lead类一样存储了工作流实例ID。
使用PersistAssignment扩展
打开Activities文件夹中的CreateAssignment.cs文件,对这个文件代码作为同CreateLead、AssignLead活动相同的修改。具体的实现代码如清单15-5所示:
Listing 15-5. Modified Implementation of CreateAssignment.cs
using System;
using System.Activities;
using System.Activities.Tracking;
using System.Linq;
using System.Data.Linq;
using System.Transactions;
using LeadGenerator.Extensions;
namespace LeadGenerator.Activities
{
/*****************************************************/
// This custom activity creates an Assignment class
// using the input parameters (LeadID and AsignedTo).
/*****************************************************/
public sealed class CreateAssignment : NativeActivity
{
public InArgument<int> LeadID { get; set; }
public InArgument<string> AssignedTo { get; set; }
protected override void Execute(NativeActivityContext context)
{
// Get the connection string
DBExtension ext = context.GetExtension<DBExtension>();
if (ext == null)
throw new InvalidProgramException("No connection string available");
// Create a data context
LeadDataDataContext dc = new LeadDataDataContext(ext.ConnectionString);
// Enlist on the current transaction
RuntimeTransactionHandle rth = new RuntimeTransactionHandle();
rth = context.Properties.Find(rth.ExecutionPropertyName)
as RuntimeTransactionHandle;
if (rth != null)
{
Transaction t = rth.GetCurrentTransaction(context);
// Open the connection, if necessary
if (dc.Connection.State == System.Data.ConnectionState.Closed)
dc.Connection.Open();
dc.Connection.EnlistTransaction(t);
}
// Create an Assignment class and populate its properties
Assignment a = new Assignment();
dc.Assignment.InsertOnSubmit(a);
a.WorkflowID = context.WorkflowInstanceId;
a.LeadID = LeadID.Get(context);
a.DateAssigned = DateTime.Now;
a.AssignedTo = AssignedTo.Get(context);
a.Status = "Assigned";
a.DateDue = DateTime.Now + TimeSpan.FromDays(5);
PersistAssignment persist = context.GetExtension<PersistAssignment>();
persist.AddAssignment(context.WorkflowInstanceId, a, "Insert");
}
}
}
修改应用程序
打开AddLead.xaml.cs文件并在SetupInstance()方法中添加工作流实例的扩展,具体代码如下所示:
// Setup persistence
i.Extensions.Add(new PersistLead(_connectionString));
i.Extensions.Add(new PersistAssignment(_connectionString));
对于AssignLead活动来说Lead参数类型已经变为InOutArgumen,打开LeadGeneratorWF.cs文件,并对AssignLead
活动的接收参数Lead的类型修改为InOutArgument。
运行应用程序
现在你可以运行应用程序,程序运行和前几章相似,但有点细微的差别。在第14章实现中,AssignLead和CreateAssignment
活动之间有20秒的延迟,直到延迟结束后Lead表的更新操作才被提交,这意味着记录被锁定了。如果你尝试在数据库中查询Lead表,将会等待锁释放后才会显示出查询结果。
你可以打开第14章的项目并运行程序,创建一个销售意向并分配代理人,然后你在数据库中执行Lead表查询时,查询结果直到应用程序事务提交后才显示出来。
现在运行第15章的程序并进行同样的操作,你将看到对Lead表的查询很快去显示出来,但分配代理人并未显示,这是因为在TransactionScope活动完成之前被持久化的流程并未执行数据库操作。
使用这种方式优点就是可以保证程序更新和工作流一致。这消除了流程活动可能已经完成但数据并未实际保存到数据的情况。另一种好处就是保证应用程序的所有更新都在同一时间内完成。如果整个流程中有多个更新,将锁定数据库同时等待其它的更新完成,使用这种方法,在所有的处理完成之前将多个更新排队而不提交改变。
使用这种方式尤其好的一个方面就是,所有的数据库操作都是由单独的类来处理。比如CreateLead和AssignLead活动将不再执行数据库持久化操作,这将使得你在设计工作流是关注于业务逻辑而不用考虑关于数据库的更改操作。数据库的更改已经作为逻辑化和物理化的一部分附加到持久处理。而不再是工作流的业务逻辑。