技术开发 频道

LinQ to SQL 并发与事务


【IT168技术文档】

  首先使用下面的SQL语句查询数据库的产品表:
select * from products where categoryid=1
  查询结果如下图:

  为了看起来清晰,我已经事先把所有分类为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); 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 in query) p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1); try { ctx.SubmitChanges(ConflictMode.ContinueOnConflict); } catch (ChangeConflictException) { foreach (ObjectChangeConflict cc in ctx.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 in ctx.ChangeConflicts) { Product p = (Product)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; 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,说明在第一条记录失败后,后续的并发冲突就不再处理了。

  事务处理

  Linq to sql在提交更新的时候默认会创建事务,一部分修改发生错误的话其它修改也不会生效:
ctx.Customers.Add(new Customer { CustomerID = "abcdf", CompanyName = "zhuye" }); ctx.Customers.Add(new Customer { CustomerID = "abcde", CompanyName = "zhuye" }); ctx.SubmitChanges();
  假设数据库中已经存在顾客ID为“abcde”的记录,那么第二次插入操作失败将会导致第一次的插入操作失效。执行程序后会得到一个异常,查询数据库发现“abcdf”这个顾客也没有插入到数据库中。
  如果每次更新后直接提交修改,那么我们可以使用下面的方式做事务:
if (ctx.Connection != null) ctx.Connection.Open(); DbTransaction tran = ctx.Connection.BeginTransaction(); ctx.Transaction = tran; try { CreateCustomer(new Customer { CustomerID = "abcdf", CompanyName = "zhuye" }); CreateCustomer(new Customer { CustomerID = "abcde", CompanyName = "zhuye" }); tran.Commit(); } catch { tran.Rollback(); } private void CreateCustomer(Customer c) { ctx.Customers.Add(c); ctx.SubmitChanges(); }
  运行程序后发现增加顾客abcdf的操作并没有成功。或者,我们还可以通过TransactionScope实现事务:
using (TransactionScope scope = new TransactionScope()) { CreateCustomer(new Customer { CustomerID = "abcdf", CompanyName = "zhuye" }); CreateCustomer(new Customer { CustomerID = "abcde", CompanyName = "zhuye" }); scope.Complete(); }
  事务是一个原子的工作单位,必须完整的完成单位里的所有工作,要么全部执行,要么全部都不执行。如果提交事务,则事务执行成功;如果回滚事务,则事务执行失败。 事务具备4个基本特性--ACID(原子性、一致性、孤立性和持久性)。

  在Linq to SQL中,有三种方法创建事务:

  1. 如果没有指定任何事务,那么当调用SubmitChanges方法时,DataContext会默认创建一个事务。
  2. 使用TransactionScope创建轻量级事务
  3. 给DataContext的Transaction属性指定事务

  下面我用代码分别来说明这几种创建事务的方法,以Northwind数据库为例,先来看看直接使用SubmitChanges:
NorthwindDataContext ctx = new NorthwindDataContext(); Customer c1 = new Customer { CustomerID = "TESTA", CompanyName = "testa's company" }; Customer c2 = new Customer { CustomerID = "TESTBC", CompanyName = "testb's company" }; ctx.Customers.Add(c1); ctx.Customers.Add(c2); ctx.SubmitChanges();
  上面这段代码中,先创建了两个Customer对象然后添加到DataContext里面,其中的c2的CustomerID赋值为"TESTBC",长度为六个字符,而数据库中该字段约束为5个字符长度,这样在SubmitChanges的时候应该会有异常抛出。果然在执行的时候抛出了 SqlException,提示字符将被截断。

  再通过Sql Server管理器可以看到上面这两条数据都没有被插入到数据库中。通过Reflector可以发现在SubmitChanges的时候,Linq to SQL默认创建了一个孤立级别为Read Committed的事务(它表示已提交的更新在事务间是可见的,具体有哪些孤立级别可以参考ADO.NET相关资料):
public virtual void SubmitChanges(ConflictMode failureMode) { ... transaction = this.provider.Connection.BeginTransaction(IsolationLevel.ReadCommitted); this.provider.Transaction = transaction; }
  如果不想使用默认的事务设置,比如想改变事务的孤立级别,我们可以给DataContext的Transaction属性赋值,以此使用自定义的事务。
ctx.Transaction = ctx.Connection.BeginTransaction(System.Data.IsolationLevel.Serializable); try { ctx.SubmitChanges(); ctx.Transaction.Commit(); } catch { ctx.Transaction.Rollback(); throw; } finally { ctx.Transaction = null; }
  最后一种方式是通过TransactionScope创建轻量级事务,就像在ADO.NET中使用一样:
using (TransactionScope scope = new TransactionScope()) { ctx.SubmitChanges(); scope.Complete(); }
  上面的例子看起来似乎多此一举,因为在SubmitChanges中会创建默认的事务,但是改成下面这样,就只能使用自定义的事务了:
using (TransactionScope scope = new TransactionScope()) { ctx.ExecuteCommand("exec ...."); ctx.ExecuteCommand("exec ...."); ctx.ExecuteCommand("exec ...."); ctx.SubmitChanges(); scope.Complete(); }
  不管ExecuteCommand里面执行了哪些操作,我们都能够指明这些行为和SubmitChanges处于同一个事务中。
0
相关文章