none
试图通过Boolean参数控制并发冲突的检查方式(LINQ to SQL) RRS feed

  • 常规讨论

  • 作者:光脚丫思考
    版权所有,转载请注明出处!^_^
    原文发布:http://blog.csdn.net/GJYSK/archive/2010/10/11/5933250.aspx

    此研习用到的测试代码可通过以下两个地址下载,如果不能下载,请留言通知我。
    下载地址01:http://download.csdn.net/source/2747401
    下载地址02:http://u.115.com/file/f26716bcc2

    如果你只想快速的查看测试代码的主题部分,或者想更具体的了解测试的详细记录,则可以参看另一篇名为《 N层研习中的测试代码01 》的博文,地址是:
    http://blog.csdn.net/GJYSK/archive/2010/10/11/5933279.aspx

    微软MSDN带来的困惑
    微软的MSDN确实是个好东西,其中提供的技术资料,只要你善于去学习,必定是会受益匪浅的。毕竟,技术是微软的,它所提供的资料多多少少还是占据着一些优势的。这是值得称赞的地方。然而,MSDN也并非就是一个完美无瑕的东西,有一些不足之处某种情况下足可以让你疯狂,至少所带来的困惑足可以让你茶不思饭不想的了。呵呵,前提是你对困惑有着极强的解决欲望,否则也只能是无动于衷了!^_^
    这几天我就一直被《关于如何在N层体系架构中使用LINQ to SQL进行开放式并发检查》的问题困惑着。实际上,微软在这方面所作出的说明是颇为详细的,将其中的前因后果道的是一清二楚。然而,关于如何具体的实现这些方案,MSDN所提供的代码是那么的一带而过。虽然具有高度的总结性,但当你自己想要去具体的实现这些方案的时候,便会发现事实上并非是如此的简单。没错!我的承认,微软的说明是够详细的了,但是,相对于我在测试过程中产生的接二连三的疑惑,有些确实应该披露的信息真的是没有披露出一丝一毫,于是,剩下的事情就该你自己去慢慢的琢磨思考,如果幸运的话,最终你会找到问题的答案,揭开那个困惑你的谜团。好了,现在让我们回到正题……

    两种并发冲突检查方式
    在N层体系架构中,LINQ to SQL提供了两种进行并发冲突检查的方式:一种是基于时间戳的方式,另一种就是基于原始值的方式。
    所谓基于时间戳的方式,实际上就是在试图对数据进行更新和删除操作的时候,首先会判断所删除的那行记录的时间戳列的值是否已经发生改变。这个改变自然是相对于当前用户读取数据时的时间戳列值了。如果,在用户读取数据并进行修改,但还没有提交到数据库的这段时间里,有其他的用户对数据进行了任何的修改操作,那么,数据库都会自动更新时间戳列的值,于是,当前用户尝试用原始的时间戳值和数据库修改后的时间戳值进行比较是,自然就不会匹配,这样,就可以判断已经发生了并发冲突。如此说,可能有些晦涩,不如看看下面的这个SQL命令,此命令是LINQ to SQL所生成的。
    以上的SQL代码所需要注意的是那个WHERE子句。其实,添加上这个WHERE子句,你就可以这样理解:当试图更新数据的时候,会判断CustomerID和Version的值是否和读取时是相同的。CustomerID的值自然是不会被改变的,因为它是主键列成员,在LINQ to SQL中是不允许对其进行修改的。因此,CustomerID的值保持不变。而Version的值我们当然也不会去修改。也就是说继续保留读取时的原始值。可是,这说的是在内存中的对象,而并非数据库中的时间戳列的值。如果别的用户更新了这条数据,此行数据的时间戳值必定发生改变。这样一来上面的SQL语句就不会被成功执行,因为LINQ to SQL找不到要更新的行了。于是,LINQ to SQL就认为这种情况是发生了并发冲突。
    而基于原始值的方式进行并发冲突检查,则稍微要显得复杂一些,这具体可体现到所生成的SQL命令和实际的编码中。所谓的基于原始值的方式,等于是在更新和删除数据之前,几乎是对每一列的原始值和数据库当前值进行比较,如果被检查的任何一列的数据当前值和原始值是不匹配的,LINQ to SQL就认为发生了并发冲突。下面是基于原始值的方式进行并发冲突检查时,LINQ to SQL生成的SQL命令。

    UPDATE [dbo].[Customers]  
    SET [Address] = @p9  
    WHERE ([CustomerID] = @p0) AND ([ContactName] = @p1) AND ([ContactTitle] = @p2) AND ([Address] = @p3) AND ([City] = @p4) AND ([Region] IS NULL) AND ([PostalCode] = @p5) AND ([Country] = @p6) AND ([Phone] = @p7) AND ([Fax] = @p8)  
    -- @p0: Input NChar (Size = 5; Prec = 0; Scale = 0) [ALFKI]  
    -- @p1: Input NVarChar (Size = 16; Prec = 0; Scale = 0) [Maria Anders1211]  
    -- @p2: Input NVarChar (Size = 24; Prec = 0; Scale = 0) [Sales Representative1211]  
    -- @p3: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [Obere Str. 57]  
    -- @p4: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [Berlin]  
    -- @p5: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [12209]  
    -- @p6: Input NVarChar (Size = 7; Prec = 0; Scale = 0) [Germany]  
    -- @p7: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [030-0074321]  
    -- @p8: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [030-0076545]  
    -- @p9: Input NVarChar (Size = 14; Prec = 0; Scale = 0) [Obere Str. 571]  
    -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8 
    UPDATE [dbo].[Customers]
    SET [Address] = @p9
    WHERE ([CustomerID] = @p0) AND ([ContactName] = @p1) AND ([ContactTitle] = @p2) AND ([Address] = @p3) AND ([City] = @p4) AND ([Region] IS NULL) AND ([PostalCode] = @p5) AND ([Country] = @p6) AND ([Phone] = @p7) AND ([Fax] = @p8)
    -- @p0: Input NChar (Size = 5; Prec = 0; Scale = 0) [ALFKI]
    -- @p1: Input NVarChar (Size = 16; Prec = 0; Scale = 0) [Maria Anders1211]
    -- @p2: Input NVarChar (Size = 24; Prec = 0; Scale = 0) [Sales Representative1211]
    -- @p3: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [Obere Str. 57]
    -- @p4: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [Berlin]
    -- @p5: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [12209]
    -- @p6: Input NVarChar (Size = 7; Prec = 0; Scale = 0) [Germany]
    -- @p7: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [030-0074321]
    -- @p8: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [030-0076545]
    -- @p9: Input NVarChar (Size = 14; Prec = 0; Scale = 0) [Obere Str. 571]
    -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

    再看看这个WHERE子句,你就不得不佩服了。是不是几乎对每一个列都进行了比对呢?那只是一个列的数据和原始值不相同了,这个SQL语句肯定找不到要更新的行。于是乎,LINQ to SQL就认为又一次的产生了并发冲突。
    以上是我对LINQ to SQL两种检查并发冲突的方式的简单说明,只是个人的理解而已。如果有什么不对的地方,还请各位给予指正!谢谢!

    困惑的开始
    一开始我就说过,MSDN在N层体系架构中使用LINQ to SQL进行并发冲突检查的所给出的示例代码太少。虽然你会看见有几段的代码,但太过于概括了,很多的信息都没有说明清楚。我摘录其中的一段:
    删除操作涉及到开放式并发检查,并且必须首先将要删除的对象附加到新的数据上下文。在此示例中,Boolean参数设置为false,以指示该对象没有时间戳(RowVersion)。如果数据库表确实为每个记录生成了时间戳,则并发检查会简单得多(特别是对客户端而言)。 只需传入原始对象或已修改的对象,并将Boolean参数设置为true。
    上面这段话是直接从MSDN上复制过来。下面是它所提供的示例代码,我稍微做了一点修改,但基本上和他原来的情况是一致的。

    public void DeleteProduct(Product ProductObject)  
    {  
    NorthwindDataContext db =   
    new NorthwindDataContext(@"C:\LINQ\Northwind.mdf");  
        db.Products.Attach(ProductObject, false);  
        db.Products.DeleteOnSubmit(ProductObject);  
        db.SubmitChanges();  

    public void DeleteProduct(Product ProductObject)
    {
    NorthwindDataContext db =
    new NorthwindDataContext(@"C:\LINQ\Northwind.mdf");
        db.Products.Attach(ProductObject, false);
        db.Products.DeleteOnSubmit(ProductObject);
        db.SubmitChanges();
    }
     
    结合上面这段示例代码,我对从MSDN复制的那段中提到的那个Boolean参数做一些说明。实际上,它所指的就是代码中的那个Attach()方法的一个参数,这是其中的一个重载方法。不晓得,各位看到前面那个说明的时候,会是怎样理解的呢?我说说我自己的理解,大家看看,我的理解是否正确的。
    如果对象有没有时间戳,就将Boolean参数设置为false,如果有的话,就设置为true。但事实上,如果你查看Attach()方法对这个参数所做的注释便会发现,此参数的重要意义并不是用来指示实体类是否有版本列成员,而是用来表示所附加的对象是否被修改。如果附加的是没有被修改的对象,那么就将其设置为false;反之,则设置为true。
    我尝试着去理解是什么原因产生了这种细微的描述差异呢。幸运的是我想我知道为什么会有这样的区别。而正是因为了有这个幸运,接着就带来了一系列的困惑。先说说为什么会有这种细微的差异呢?
    就以前面的示例代码做说明吧。当我们调用上面用到的的Attach()重载方法时,将对象附加给数据上下文的时候,如果Boolean参数设置为true的话,其实就是告诉数据上下文,此对象已经被修改过了。不管我们采用什么样的方式附加对象,LINQ to SQL在将修改提交到数据库的时候,都是要进行开放式并发检查的。不晓得有没有什么参数可以取消这个开放式并发检查呢?目前为止我还不知道。既然它非要进行开放式并发检查,而进行这种检查的方式,无非就只有两种:基于时间戳的方式和基于原始值的方式。这样一来如果我们照着前面的做法将对象作为已修改对象附加给数据上下文的话,肯定是没法再为其提供原始值了。即使你通过其他的方式将原始值提供给数据上下文的话,LINQ to SQL也会认为在你将对象附加的时候,有些修改它是没有跟踪到的。因为,一附加给数据上下文的时候就指示对象已被修改。因此它就会一股脑的认为对象已经被修改,且没有可跟踪的原始值。这样一来,想要使用基于原始值的方式自然是不可行了。也许LINQ to SQL正是在这种无可奈何的情况下不得不使用基于时间戳的方式。
    这种推测正确吗?无论是当时,还是现在我都认为这种推测应该是合情合理的。不过也只仅限于我对MSDN的那段话的理解,事实是否如此呢?咱们后面再说。
    有了上面的推测,让我自然而然地认为,到底LINQ to SQL是使用哪种方式进行开放式并发冲突检查,完全可以通过这个Boolean参数来进行控制。于是我专门编写了一个测试项目来验证自己的推测。测试的结果确实让我大大地跌破了眼镜。虽然我是不带眼镜的!^_^

    测试的说明
    测试用到的数据库是我自行定义的,其中的数据表都给添加了时间戳列。然后在VS中使用对象关系设计器基于这个有时间戳列的数据库生成了对象模型。在测试代码中,我就尝试着通过对Boolean参数的修改,然后跟踪LINQ to SQL所生成SQL语句来看看到底是使用了哪种方式进行开放式并发检查的。这其中分别对删除和更新这两个操作进行了测试。至于检索和增加数据则不存在开放式并发检查,因此相对而言要简单了许多。
    另外,是使用WCF来做这个测试的。为了方便起见将服务端和客户端做成了两个项目,再运行客户端之前,请务必先将服务端程序启动,否则自然是无法测试的了。
    测试的结果如何呢?想着当初我抓耳挠腮的情形,此时此刻我心平气和的告诉你,无论我怎么设置Boolean参数,LINQ to SQL都是一如既往的使用着基于时间戳的方式来进行开放式并发检查的。为了慎重起见,我将测试代码一遍又一遍的测试着,生怕自己在某些地方出现了纰漏,那样就直接影响我对这一问题的最终认识。然而事实却是,不行就是不行,LINQ to SQL似乎带着极度傲慢的态度告诉我:想通过设置Boolean参数就来控制我选择哪种开放式并发检查?没门!终于,我得承认,前面的推测是不成立的。于是乎,一系列困惑就开始接二连三的跑了出来。哎!过程就像是个噩梦,但结果却是让人欣喜若狂!这种感觉其实蛮好的。
    至于测试的细节问题,可以从所提供的测试代码中查看,我做了详细的注释说明。测试代码可以从这里找到:
    事情并没有到此结束,与之后事如何,且听下回分解!^_^咱们也学罗贯中拽一会!

    如果错误之处,欢迎各位的指正,并期望能够进行更为深入的交流!

    光脚丫思考!

    2010年10月11日 6:29