问题描述

此问题是通过和CSDN的网友交流获得解决的,首先对参与和关注此问题的网友表示感谢,特别是foren_whb给予了热心地、直接地帮助!谢谢你们!望日后继续进行着广泛地深入的交流!

虽然说,谁也不想遇到并发冲突这种情况,但这却是一个必然会发生的事情,因此,学习掌握它的解决办法还是很有必要的。我们自己当然可以自定义并发冲突情况的处理办法了,但LINQ to SQL本身在这个问题上也有它自己的解决办法。于是自己就想来测试测试它的这个解决办法。在这之前,首先得要模拟出并发冲突吧!然后,就根据自己所掌握的LINQ to SQL知识动手编写了一个小小的测试代码。代码如下:

// 用户A和用户B同时获取相同的记录。
var UserACustomers = dbUserA.Customers.Skip(23).Take(7);
var UserBCustomers = dbUserB.Customers.Skip(23).Take(7);

// 用户B首先修改数据并更新到数据。
foreach (var CustomerObject in UserBCustomers)
{
  CustomerObject.ContactName = "UserB";
  CustomerObject.ContactTitle = "UserB";
  CustomerObject.Country = "UserB";
  CustomerObject.City = "UserB";
  CustomerObject.Address = "UserB";
}
dbUserB.SubmitChanges();

// 然后才是用户A修改数据,并尝试更新到数据库,此时将引发并发冲突。
foreach (var CustomerObject in UserACustomers)
{
  CustomerObject.ContactName = "UserA";
  CustomerObject.ContactTitle = "UserA";
  CustomerObject.Country = "UserA";
  CustomerObject.City = "UserA";
  CustomerObject.Address = "UserA";
}
dbUserA.SubmitChanges(ConflictMode.FailOnFirstConflict);


通常情况下,如果按照这个代码来执行的话,应该是会引发并发冲突的。然而,事实却并非像想象的那样。如果程序是按着代码编写的顺序来执行的话,那在两个数据上下文调用Skip()和Take()运算符的时候,就应该获取相关数据记录的。但是,LINQ to SQL却有一个延迟执行的问题,而这个Skip()和Take()运算符刚好就是延迟执行的运算符。延迟执行是个什么概念呢?以上面的代码为例,当我们调用这两个运算符的时候,实际上并没有真正的从数据库中获取数据,当代码运行到foreach数据的时候,此时LINQ to SQL才去数据库中读取数据。这样一来就存在这样的一个问题了:
之所以引发并发冲突,是因为用户在读取到要更新的数据记录后、提交更新操作之前的这段时间里,数据库中的记录已经被修改了。既然Skip()和Take()运算符是延迟执行的,这样当用户B把数据更新到数据库之后,用户A才从数据库获取数据,这样一来,无论如何都不会引发并发冲突了。一时间竟没了主意。现在想起来,可真是够笨的了!
后来在论坛上发帖子,希望其他的网友能够对这个问题有研究!

解决方法一
后来CSDN网友foren_whb指出可以使用ToList()这个运算符,说这个运算符是理解执行的。
我把上面的代码修改为这样的:

var UserACustomers = dbUserA.Customers.Skip(23).Take(7).ToList();
var UserBCustomers = dbUserB.Customers.Skip(23).Take(7).ToList();


终于搞出了一个并发冲突了!^_^
想让它出来的时候,它还不出来;不想让它出来的时候,老跑出来捣乱,真是可恶至极!

解决方法二
后来,又被我想到了另外一个解决办法:
在LINQ to SQL中所修改的数据要向被保存到数据库中,必须调用DataContext.SubmitChanges()这个方法,否则的话,所做的修改仅仅是内存中的副本,而并非数据库中的数据。既然如此,在调用foreach修改数据之后,暂且不要调用这个方法,这样一来,用户A和用户B修改的数据都不会被提交的。直到用户A也调用foreach语句之后,再提交更新操作。这样也能引发并发冲突!代码如下:

v// 用户A和用户B同时获取相同的记录。
var UserACustomers = dbUserA.Customers.Skip(23).Take(7);
var UserBCustomers = dbUserB.Customers.Skip(23).Take(7);

// 用户B首先修改数据并更新到数据。
foreach (var CustomerObject in UserBCustomers)
{
  CustomerObject.ContactName = "UserB";
  CustomerObject.ContactTitle = "UserB";
  CustomerObject.Country = "UserB";
  CustomerObject.City = "UserB";
  CustomerObject.Address = "UserB";
}

// 然后才是用户A修改数据,并尝试更新到数据库,此时将引发并发冲突。
foreach (var CustomerObject in UserACustomers)
{
  CustomerObject.ContactName = "UserA";
  CustomerObject.ContactTitle = "UserA";
  CustomerObject.Country = "UserA";
  CustomerObject.City = "UserA";
  CustomerObject.Address = "UserA";
}

dbUserB.SubmitChanges();
dbUserA.SubmitChanges(ConflictMode.FailOnFirstConflict);


上面的代码是在用户A和用户都修改完毕数据之后,才提交他们各自上下文的SubmitChanges()方法,用户B的数据被成功更新,但用户A的更新却引发了并发冲突!^_^

延迟加载和延迟执行的区别
另外,在这个问题的交流中,使我明白了两个概念【延迟加载】和【延迟执行】。
所谓【延迟加载】指的是关系映射中相关联的数据,是在需要的时候加载呢?还是一次性的加载出来呢?
比如说,Northwind数据库中的客户(Customers)可能会有多个订单(Orders),当我们检索客户的时候,是否一次性的把此客户的所有订单都检索出来呢?如果不是,也就是说在需要的时候才去检索某个客户的订单记录,这便是延迟加载了。而立即加载的意思,就是说在检索客户的时候,一次性的把关联的订单记录都检索出来咯。这个延迟加载,或者立即加载都是有各自的好处和缺点的。
可以通过DataContext.DeferredLoadingEnabled来设置是否延迟加载。
而【延迟执行】的意思就是:不会立即执行,而是等到需要数据的时候,它才会去执行。
比如,本例中的Skip()和Take()运算符,当我们调用的时候它就没有获取数据,而是到foreach语句的时候,才去获取。
向声明性查询基本上都是延迟执行的。
呵呵,这两个概念之前读过,但是真是没有好好的区别它们。以至于我在提问的时候,把延迟执行,还说成是延迟加载呢!