一步一步学Linq to sql(七):并发与事务
前言
检测并发
首先使用下面的SQL语句查询数据库的产品表:
select UnitPrice,UnitsInStock,* from Products where categoryID=1
为了看起来清晰,我已经事先把所有分类为1产品库存修改为相同值了。然后执行下面的程序:
static void Main(string[] args) { NorthWindDataContext ctx = new NorthWindDataContext(); var query = from p in ctx.Products where p.CategoryID == 1 select p; foreach (var p in query) p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1); ctx.SubmitChanges(); // 在这里设断点 }
我们使用调试方式启动,由于设置了断点,程序并没有进行更新操作。此时,我们在数据库中运行下面的语句:
update products set unitsinstock = 2, unitprice= unitprice + 1 where categoryid = 1
然后在继续程序,会得到修改并发(乐观并发冲突)的异常,提示要修改的行不存在或者已经被改动。
当客户端提交的修改对象自读取之后已经在数据库中发生改动,就产生了修改并发。解决并发的包括两步,一是查明哪些对象发生并发,二是解决并发。如果你仅仅是希望更新时不考虑并发的话可以关闭相关列的更新验证,这样在这些列上发生并发就不会出现异常:
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_UnitsInStock", DbType="SmallInt",UpdateCheck= UpdateCheck.Never)] public System.Nullable<short> UnitsInStock { get { return this._UnitsInStock; } set { if ((this._UnitsInStock != value)) { this.OnUnitsInStockChanging(value); this.SendPropertyChanging(); this._UnitsInStock = value; this.SendPropertyChanged("UnitsInStock"); this.OnUnitsInStockChanged(); } } }
主要就是添加了UpdateCheck= UpdateCheck.Never
为这一列标注不需要进行更新检测。假设现在产品库存是32。那么,我们启动程序(设置端点),然后运行UPDATE语句,把库存-2,然后产品库存为30了,继续程序可以发现库存为31。库存最终剪掉1是我们程序之后更新的功劳。当在同一个字段上(库存)发生并发冲突的时候,默认是最后的那次更新获胜。
解决并发
如果你希望自己处理并发的话可以把前面对列的定义修改先改回来,看下面的例子:
var query = from p in ctx.Products where p.CategoryID == 1 select p; foreach (var p in query) p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1); try { ctx.SubmitChanges(ConflictMode.ContinueOnConflict); } catch (ChangeConflictException) { foreach (ObjectChangeConflict cc in ctx.ChangeConflicts) { Products p = (Products)cc.Object; Console.Write(p.ProductID + "<br/>"); cc.Resolve(RefreshMode.OverwriteCurrentValues); // 放弃当前更新,所有更新以原先更新为准 } } ctx.SubmitChanges(); Console.ReadLine();
首先可以看到,我们使用try{}catch{}来捕捉并发冲突的异常。在SubmitChanges的时候,我们选择了ConflictMode.ContinueOnConflict选项。也就是说遇到并发了还是继续。在catch{}中,我们从ChangeConflicts中获取了并发的对象,然后经过类型转化后输出了产品ID,然后选择的解决方案是RefreshMode.OverwriteCurrentValues。也就是说,放弃当前的更新,所有更新以原先更新为准。
我们来测试一下,假设现在产品库存是32。那么,我们启动程序(在ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE语句,库存-2,然后库存为30了,继续程序可以发现库存是30。之前SQL语句库存-2生效了,而我们程序的更新(库存-1)被放弃了。在页面上也显示了所有分类为1的产品ID(因为我们之前的SQL语句是对所有分类为1的产品都进行修改的)。
// 摘要: // 定义 Overload:System.Data.Linq.DataContext.Refresh 方法如何处理开放式并发冲突。 public enum RefreshMode { // 摘要: // 强制 Overload:System.Data.Linq.DataContext.Refresh 方法使用从数据库检索的值替换原始值。不会修改当前值。 KeepCurrentValues = 0, // // 摘要: // 强制 Overload:System.Data.Linq.DataContext.Refresh 方法保留已更改的当前值,但将其他值更新为数据库值。 KeepChanges = 1, // // 摘要: // 强制 Overload:System.Data.Linq.DataContext.Refresh 方法使用数据库中的值重写所有当前值。 OverwriteCurrentValues = 2, }
处理并发时,从这三个选择合适的就可以了。
我们甚至还可以针对不同的字段进行不同的处理策略:
var query = from p in ctx.Products where p.CategoryID == 1 select p; foreach (var p in query) p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1); try { ctx.SubmitChanges(ConflictMode.ContinueOnConflict); } catch (ChangeConflictException) { foreach (ObjectChangeConflict cc in ctx.ChangeConflicts) { Products p = (Products)cc.Object; foreach (MemberChangeConflict mc in cc.MemberConflicts) { ///获取发生冲突的当前值 string currVal = mc.CurrentValue.ToString(); ///获取发生冲突的原始值 string origVal = mc.OriginalValue.ToString(); ///获取发生冲突的数据库值 string databaseVal = mc.DatabaseValue.ToString(); MemberInfo mi = mc.Member; string memberName = mi.Name; Console.WriteLine(p.ProductID + " " + mi.Name + " " + currVal + " " + origVal + " " + databaseVal); if (memberName == "UnitsInStock") mc.Resolve(RefreshMode.KeepCurrentValues); // 放弃原先更新,所有更新以当前更新为准 else if (memberName == "UnitPrice") mc.Resolve(RefreshMode.OverwriteCurrentValues); // 放弃当前更新,所有更新以原先更新为准 else mc.Resolve(RefreshMode.KeepChanges); // 原先更新有效,冲突字段以当前更新为准 } } } Console.ReadLine();
最后,我们把提交语句修改为:
ctx.SubmitChanges(ConflictMode.FailOnFirstConflict);
事务处理
Linq to sql在提交更新的时候默认会创建事务,一部分修改发生错误的话其它修改也不会生效:
ctx.Products.InsertOnSubmit(new Products { ProductID =1, ProductName = "zhuye1" }); ctx.Products.InsertOnSubmit(new Products { ProductID = 2, ProductName = "zhuye" }); ctx.SubmitChanges();
假设数据库中已经存在产品ID为2的记录,那么第二次插入操作失败将会导致第一次的插入操作失效。执行程序后会得到一个异常,查询数据库发现1这个产品也没有插入到数据库中。
如果每次更新后直接提交修改,那么我们可以使用下面的方式做事务:
if (ctx.Connection != null) ctx.Connection.Open(); DbTransaction tran = ctx.Connection.BeginTransaction(); ctx.Transaction = tran; try { CreateProduct(new Products { ProductID = 1, ProductName = "zhuye1" }); CreateProduct(new Products { ProductID = 2, ProductName = "zhuye2" }); tran.Commit(); } catch { tran.Rollback(); }
private static void CreateProduct(Products c) { ctx.Products.InsertOnSubmit(c); ctx.SubmitChanges(); }
运行程序后发现增加产品1的操作并没有成功。或者,我们还可以通过TransactionScope实现事务:
//使用之前必须添加对 System.Transactions.dll 的引用。 using (TransactionScope scope = new TransactionScope()) { CreateProduct(new Products { ProductID = 1, ProductName = "zhuye1" }); CreateProduct(new Products { ProductID = 2, ProductName = "zhuye2" }); scope.Complete(); }
示例代码下载地址https://files.cnblogs.com/aehyok/LinqApp.rar