首先使用下面的SQL 语句查询数据库的产品表:
select * from products wherecategoryid=1
为了看起来清晰,我已经事先把所有分类为1 产品的价格和库存修改为相同值了。然
后执行下面的程序:
var query = from p in ctx.Products where p.CategoryID == 1 select p;
foreach (var p inquery)
p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1);
ctx.SubmitChanges(); // 在这里设断点
我们使用调试方式启动,由于设置了断点,程序并没有进行更新操作。此时,我们在数
据库中运行下面的语句:
update products
set unitsinstock = unitsinstock -2, unitprice= unitprice + 1
where categoryid = 1
然后在继续程序,会得到修改并发(乐观并发冲突)的异常,提示要修改的行不存在或
者已经被改动。当客户端提交的修改对象自读取之后已经在数据库中发生改动,就产生了修
改并发。解决并发的包括两步,一是查明哪些对象发生并发,二是解决并发。如果你仅仅是
希望更新时不考虑并发的话可以关闭相关列的更新验证,这样在这些列上发生并发就不会出
现异常:
[Column(Storage="_UnitsInStock", DbType="SmallInt", UpdateCheck =
UpdateCheck.Never)]
[Column(Storage="_UnitPrice", DbType="Money", UpdateCheck = UpdateCheck.Never)]
为这两列标注不需要进行更新检测。假设现在产品价格和库存分别是27 和32。那么,
我们启动程序(设置端点),然后运行UPDATE语句,把价格+1,库存-2,然后价格和库
存分别为28 和30 了,继续程序可以发现价格和库存分别是28 和31。价格+1 是之前更新
的功劳,库存最终是-1 是我们程序之后更新的功劳。当在同一个字段上(库存)发生并发
冲突的时候,默认是最后的那次更新获胜。
解决并发
如果你希望自己处理并发的话可以把前面对列的定义修改先改回来,看下面的例子:
var query = from p in ctx.Products where p.CategoryID == 1 select p;
foreach (var p inquery)
p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1);
try
{
ctx.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException)
{
foreach (ObjectChangeConflict cc inctx.ChangeConflicts)
{
Product p = (Product)cc.Object;
Response.Write(p.ProductID +"<br/>");
cc.Resolve(RefreshMode.OverwriteCurrentValues); // 放弃当前更新,
所有更新以原先更新为准
}
}
ctx.SubmitChanges();
首先可以看到,我们使用try{}catch{}来捕捉并发冲突的异常。在SubmitChanges 的时
候,我们选择了ConflictMode.ContinueOnConflict 选项。也就是说遇到并发了还是继续。
在catch{}中,我们从ChangeConflicts中获取了并发的对象,然后经过类型转化后输出了
产品ID,然后选择的解决方案是RefreshMode.OverwriteCurrentValues。也就是说,放弃
当前的更新,所有更新以原先更新为准。
我们来测试一下,假设现在产品价格和库存分别是27 和32。那么,我们启动程序(在
ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE
语句,把价格+1,库存-2,然后价格和库存分别为28 和30 了,继续程序可以发现价格和
库存分别是28 和30。之前SQL 语句库存-2 生效了,而我们程序的更新(库存-1)被放弃
了。在页面上也显示了所有分类为1 的产品ID(因为我们之前的SQL 语句是对所有分类为
1 的产品都进行修改的)。
然后,我们来修改一下解决并发的方式:
cc.Resolve(RefreshMode.KeepCurrentValues);// 放弃原先更新,所有更新以当前更新为
准
来测试一下,假设现在产品价格和库存分别是27 和32。那么,我们启动程序(在
ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE
语句,把价格+1,库存-2,然后价格和库存分别为28 和30 了,继续程序可以发现价格和
库存分别是27 和31。产品价格没有变化,库存-1 了,都是我们程序的功劳,SQL 语句的
更新被放弃了。
然后,我们再来修改一下解决并发的方式:
cc.Resolve(RefreshMode.KeepChanges);// 原先更新有效,冲突字段以当前更新为准
来测试一下,假设现在产品价格和库存分别是27 和32。那么,我们启动程序(在
ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE
语句,把价格+1,库存-2,然后价格和库存分别为28 和30 了,继续程序可以发现价格和
库存分别是28 和31。这就是默认方式,在保持原先更新的基础上,对于发生冲突的字段以
最后更新为准。
我们甚至还可以针对不同的字段进行不同的处理策略:
foreach (ObjectChangeConflict cc inctx.ChangeConflicts)
{
Product p = (Product)cc.Object;
foreach (MemberChangeConflict mc incc.MemberConflicts)
{
string currVal = mc.CurrentValue.ToString();
string origVal = mc.OriginalValue.ToString();
string databaseVal = mc.DatabaseValue.ToString();
MemberInfo mi = mc.Member;
string memberName = mi.Name;
Response.Write(p.ProductID + " " + mi.Name + " " + currVal + " " + origVal +" "+
databaseVal + "<br/>");
if (memberName == "UnitsInStock")
mc.Resolve(RefreshMode.KeepCurrentValues); // 放弃原先更新,所有更新
以当前更新为准
else if (memberName == "UnitPrice")
mc.Resolve(RefreshMode.OverwriteCurrentValues); // 放弃当前更新,所有
更新以原先更新为准
else
mc.Resolve(RefreshMode.KeepChanges); // 原先更新有效,冲突字段以当
前更新为准
}
}
比如上述代码就对库存字段作放弃原先更新处理,对价格字段作放弃当前更新处理。我
们来测试一下,假设现在产品价格和库存分别是27 和32。那么,我们启动程序(在
ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE
语句,把价格+1,库存-2,然后价格和库存分别为28 和30 了,继续程序可以发现价格和
库存分别为28 和31 了。说明对价格的处理确实保留了原先的更新,对库存的处理保留了
当前的更新。页面上显示的结果如下图:
最后,我们把提交语句修改为:
ctx.SubmitChanges(ConflictMode.FailOnFirstConflict);
表示第一次发生冲突的时候就不再继续了,然后并且去除最后的ctx.SubmitChanges();
语句。来测试一下,在执行了SQL 后再继续程序可以发现界面上只输出了数字1,说明在
第一条记录失败后,后续的并发冲突就不再处理了。