第一个数据并发性异常
该顾客服务应用程序使用了几个月没有任何问题,但是突然产生了一个没有处理的异常DBConcurrencyException。本段我将解释导致该异常的环境。
第一个使用该应用程序的顾客销售服务人员Joe打开应用程序。这将初始化将数据载入数据集dsAllData并按每30分钟一次的周期来刷新数据。Joe的收件箱中有一堆文件,包括顾客传真、邮寄或者通过电子邮件发送的更改请求。他开始处理更改请求,但是经常被电话订单中断。
其间,第二个客户销售服务人员Sally到达办公室并打开了她的应用程序实例。Sally的应用程序实例也初始化从SQL Server中载数据,包括Joe所作的更新。Sally也接到了一个顾客改变电话号码的请求。该顾客先前用电子邮件发送了地址的改变情况,但是在那时不知道他的新电话号码,现在要更新记录了。Sally使用Customer Maintenance屏幕更新顾客的电话号码。当Sally改变DataGrid中的电话号码时,新号码存储在dsAllData中。当Sally确认其它的顾客信息后,她意识到原地址的改变还没有处理,因此她更新那些信息并点击Save Changes按钮,将新数据发送到SQL Server数据库。
Joe正在处理相同顾客的原始请求。当他打开Customer Maintenance屏幕时,应用程序从缓存数据集对象中读入信息。因为Sally更新顾客地址时,Joe的应用程序没有自动与数据库同步,因此他的Customer Maintenance屏幕仍然显示旧地址。Joe使用电子邮件提供的新信息改正了DataGrid中显示的信息,并点击Save Changes按钮。这样操作后出现了一个错误信息"并发性故障:更新命令影响了0个记录(Concurrency violation: the UpdateCommand affected 0 records)",应用程序崩溃了。在Joe再次打开应用程序时,他发现地址已经更新了,认为他的更改在应用程序崩溃前已经完成了。下面就是问题的代码行:
Private Sub butSave_Click (ByVal sender As System.Object, _ ByVal e As System.EventArgs) daCustomer.Update(dsAllData.Tables("Customer")) End Sub |
实际的异常是DBConcurrencyException类型产生的,它是数据适配器对象内部建立的特定功能的结果(见图6)。该数据适配器设计为把数据填充到不连接的对象(例如数据集),这样它在执行更新前能自动地检查数据寻找改变。如果下层数据被改变了,数据适配器将引发一个DBConcurrencyException异常而不是执行更新。

图6. DBConcurrency异常
执行完整性检查是相当直接的,它提高了数据表对象的性能,使它能够保持多个数据集合。当数据第一次载入数据表时,数据表中的所有数据行(DataRow)的DataRowVersion属性设置为原始的(Original)。当修改了数据行中的一列时,该行就被复制一份并标记为当前的(Current),标记为原始的行仍然没有改变。后来的所有对该数据的更改都仅仅影响当前行。当为一个数据表(或者一个数据集中的多个数据表)执行数据适配器的更新方法时,它重复所有的当前行来决定要发送给下层数据源的更新语句。作为DataRowVersion属性的补充,数据行有一个用于识别行中数据状态的RowState属性。它的可能值为Unchanged、Added、Modified、Deleted和Detached。
在决定下层数据中的哪些行需要更新后,数据适配器dsCustomer建立更新SQL Server数据库所需要的SQL语句。在图1中我使用数据集和命令构造器对象来建立需要的INSERT、 UPDATE和DELETE语句。命令构造器对象建立的UPDATE语句使用DataRowVersion值为Original的数据行副本来识别和更新数据库中的适当行。这就是说,作为使用主键值简单地识别正确行的代替,命令构造器建立一个SQL语句来查找与数据集中存储的原始值准确匹配的行。下面的代码是建立的用于更新顾客电话号码的UPDATE语句示例:
UPDATE Customer SET Phone = @p1 WHERE ((ID = @p2) AND ((FirstName IS NULL AND @p3 IS NULL) OR (FirstName = @p4)) AND ((LastName IS NULL AND @p5 IS NULL) OR (LastName = @p6)) AND ((Address IS NULL AND @p7 IS NULL) OR (Address = @p8)) AND ((Address2 IS NULL AND @p9 IS NULL) OR (Address2 = @p10)) AND ((City IS NULL AND @p11 IS NULL) OR (City = @p12)) AND ((State IS NULL AND @p13 IS NULL) OR (State = @p14)) AND ((Zip IS NULL AND @p15 IS NULL) OR (Zip = @p16)) AND ((Phone IS NULL AND @p17 IS NULL) OR (Phone = @p18))) |
该UPDATE语句使用参数而不是实际值,但是你能看到行中每列是怎样检查的。
识别了准确的行和下层数据库中的原始值后,数据适配器就可以安全地更新行了。但是,如果自从数据表被填充后数据库中某行的某个列改变了,UPDATE语句将失败,因为数据库中没有与WHERE条件中的标准匹配的行了。数据适配器决定UPDATE是否成功很简单,只需要简单地检查数据库中被更新的行的实际数量。如果没有行被更新,那么下层数据一定被改变或删除了,就产生一个数据并发性异常。这就解释了Joe试图更新顾客电话号码时接收到的有点模糊的错误消息:数据适配器检查到的实际错误不是下层数据改变了,而是没有记录被更新,标志着下层数据必定被改变了。