LINQ那些事儿(3)- 事务和并发冲突处理
LINQ2SQL采用开放式并发的机制来处理由于数据库并发修改或由于读取和修改之间存在时间差导致的数据不一致的问题。为了实现这一机制,LINQ2SQL在提交数据更改时,会先进行开放式冲突检测,并且当发现冲突后提供了一系列解决冲突的方法。
LINQ2SQL的并发冲突处理采用optimistic concurrency(开放式并发)机制。当发生数据更新时,LINQ2SQL会检查当前数据库值和查询返回的原始值是否相等,如果相等则表示数据库值未发生变化,说明未发生冲突,可以提交更新;否则当数据库值发生变化时,会抛出ChangeConflictException,等待用户决定如何处理更新。
参考资料:
开放式并发概述 http://msdn.microsoft.com/zh-cn/library/bb399373.aspx
本文会用到一些名词,为避免大家误解,先说明如下,对于某个entity object的属性:
1) 数据库值:当并发冲突发生时,数据库保存的字段值
2) 原始值:通过DataContext查询获得的字段值。由于从DataContext查询到数据更新存在时间间隔,所以原始值与数据库值可能不同。
当前值:通过entity.Property可获取的属性值。刚从DataContext查询时,当前值=原始值,但在某些操作中可能修改了当前值,所以当前值和原始值可能不同。
开放式冲突检测
可能引发Optimistic Concurrency的只有涉及Update和Delete的操作,LINQ通过执行Update或Delete的SQL语句的操作返回值(@@ROW_COUNT)来检测是否产生冲突。
我们来执行下面的代码(使用Northwnd数据库):
var context = GenerateContext(); context.Log = Console.Out; Customer customer = context.Customers.First(); customer.CompanyName = "Updating customer"; context.SubmitChanges();
产生的SQL语句如下,注意看Where子句和传递的参数
UPDATE [dbo].[Customers] SET [CompanyName] = @p10 WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) A ND ([ContactTitle] = @p3) AND ([Address] = @p4) AND ([City] = @p5) AND ([Region] IS NULL) AND ([PostalCode] = @p6) AND ([Country] = @p7) AND ([Phone] = @p8) AND ([Fax] = @p9) -- @p0: Input NChar (Size = 5; Prec = 0; Scale = 0) [ALFKI] -- @p1: Input NVarChar (Size = 19; Prec = 0; Scale = 0) [Alfreds Futterkiste] -- @p2: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [Maria Anders] -- @p3: Input NVarChar (Size = 20; Prec = 0; Scale = 0) [Sales Representative] -- @p4: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [Obere Str. 57] -- @p5: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [Berlin] -- @p6: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [12209] -- @p7: Input NVarChar (Size = 7; Prec = 0; Scale = 0) [Germany] -- @p8: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [030-0074321] -- @p9: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [030-0076545] -- @p10: Input NVarChar (Size = 17; Prec = 0; Scale = 0) [Updating customer]
为什么LINQ2SQL在生成Where语句时,不但包含了CustomerID(Primary key),还包含了其他所有的属性列呢?就是为了检测冲突,注意看传递的参数——Customer object的原始值,通过原始值和数据库值比较,判断自上次查询后数据库值是否发生了更改。当发生更改时,上面的sql语句执行返回@@ROW_COUNT=0,LINQ2SQL以此判断是否发生了开放式并发。
上面的代码是示例LINQ2SQL根据ColumnAttribute.UpdateCheck的设置来生成的Where子句的情况。除此之外,LINQ2SQL还可以根据标记为IsVersion和IsPrimary属性来生成Where子句,这样可以生成更短的SQL语句。
参考资料:
如何指定测试哪些成员是否发生并发冲突
http://msdn.microsoft.com/zh-cn/library/bb399394.aspx
如何将列表示为时间戳或版本列
http://msdn.microsoft.com/zh-cn/library/bb386957.aspx
我们来看下面的代码,你认为在执行SubmitChanges时会产生冲突吗?
var context = GenerateContext(); var customerA = context.Customers.First(); var customerB = context.Customers .Where(b => b.CustomerID == customerA.CustomerID).Single(); customerA.CompanyName = "Updating customerA"; customerB.CompanyName = "Updating customerB"; context.SubmitChanges(); Customer customerC = context.Customers .Where(c => c.CustomerID == customerA.CustomerID).Single(); Console.WriteLine(customerC.CompanyName);
不会! CompanyName被更新为”Updating customerB”。这是因为在DataContext对象中缓存了查询返回的entity object的引用,在下一次查询时,如果发现缓存中已经存在Primary Key与数据库查询返回数据的Primary Key相同的对象时,不再重新映射,而是直接返回已经缓存的对象,所以customerA和customerB是指向同一个对象的,这样就保证了属于同一个DataContext对象的entity object,在SubmitChanges时不会产生开放式冲突。
Console.WriteLine(customerA == customerB); // 输出为True
开放式并发处理
在检测到并发冲突后,DataContext将抛出ChangeConfilictException,通过对异常的处理,用户可以决定如何处理并发冲突。下面代码示例了发生Optimistic Concurrency的情况:
var contextA = GenerateContext(); // 用contextB来模拟另外一个用户更新 var contextB = GenerateContext(); var customerA = contextA.Customers.First(); customerA.CompanyName = "Updating Value from A"; var customerB = contextB.Customers .Where(c => c.CustomerID == customerA.CustomerID).Single(); customerB.CompanyName = "Updating Value from B"; contextB.SubmitChanges(); try { // 发生开放式冲突 contextA.SubmitChanges(); } catch (ChangeConflictException) { // 在此添加代码处理冲突 }
由于customerA和customerB都是指向数据库的同一条数据,在contextB.SubmitChanges提交后,contextA.SubmitChanges就发生了冲突。
参考资料:
如何检测和解决提交冲突
下面我们接着讨论事务处理。在我们没有显式的开启事务时,DataContext.SubmitChanges在提交时会隐式创建事务,我们看看下面这段代码:
var context = GenerateContext(); Customer customerA = new Customer { CustomerID = "K.K" }; Customer customerB = new Customer { CustomerID = "K.K" }; context.Customers.InsertOnSubmit(customerA); context.Customers.InsertOnSubmit(customerB); try { context.SubmitChanges(); } catch (Exception) { Console.WriteLine(context.Customers.Where(c => c.CustomerID == "K.K").Count()); // 输出0 }
由于customerA和customerB的CustomerID赋值相同,造成了Primary Key的冲突,所以customerB插入失败。但由于DataContext.SubmitChanges自动创建事务,所以customerB插入失败造成全部操作回滚,customerA也没有插入。
DataContext.SubmitChanges可能涉及多个entity object的更新,在实际应用中,一起提交若干记录的更新有两种情况:
1) 需要事务完整约束的,如销售++,库存--
2) 不需要事务约束的。如程序运行时把产生的日志放到cache里,由专门负责日志持久化的Thread每隔一段时间把Cache里的多条日志提交保存。
DataContext.SubmitChanges(ConflictMode)可通过指定ConflictMode来满足上面的两种情况:
ConflictMode取值 |
说明 |
FailOnFirstConflict |
默认参数,一旦发生ChangeConflictException,停止SubmitChanges操作,回滚所有SubmitChanges执行的操作。满足情况1) |
ContinueOnConflict |
当发生ChangeConflictException时,继续执行下一个对象的更新操作,直至所有对象都更新完毕,返回所有发生的ChangeConflictException。满足情况2) |
除了让DataContext.SubmitChanges隐式创建事务外,我们也可以显式使用事务来控制更新操作。下面两段代码分别示例了显示事务的使用:
IDbTransaction version:
var context = GenerateContext(); Customer customerA = new Customer { CustomerID = "K.K" }; Customer customerB = new Customer { CustomerID = "K.K" }; context.Customers.InsertOnSubmit(customerA); context.Customers.InsertOnSubmit(customerB); context.Transaction = context.Connection.BeginTransaction(); try { context.SubmitChanges(); context.Transaction.Commit(); } catch (Exception) { context.Transaction.Rollback(); }
System.Transactions.TransactionScope version:
var context = GenerateContext(); Customer customerA = new Customer { CustomerID = "K.K" }; Customer customerB = new Customer { CustomerID = "K.K" }; context.Customers.InsertOnSubmit(customerA); context.Customers.InsertOnSubmit(customerB); try { using (TransactionScope trans = new TransactionScope()) { context.SubmitChanges(); trans.Complete(); } } catch (Exception) {}
参考资料:
事务(LINQ 2 SQL)http://msdn.microsoft.com/zh-cn/library/bb386995.aspx
Transactionscope troubleshooting http://space.itpub.net/13770856/viewspace-410186
链接
1、 LINQ那些事儿(1)- 定义从关系数据库到Entity Class的映射
2、 LINQ那些事儿(2)- 简单对象的CRUD操作和Association的级联操作
4、 LINQ那些事儿(4)- Query Expression和Query Operator
6、 LINQ那些事儿(6)- DataContext的对象生命周期管理
7、 LINQ那些事儿(7)- 通过自定义IEnumerable<T>来扩展LINQ
8、LINQ那些事儿(8)- 通过自定义IQueryable<T>和IQueryableProvider来扩展LINQ
All the posts in this blog are provided "AS IS" with no warranties, and confer no rights. Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution 2.5 China Mainland License.