First we try, then we trust

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

本文实验用代码请从这里下载:UpdateUsingDataAdapter.rar

先在SQL Server 2000中建立一名为DBApp的数据库,然后用查询分析器执行SQL-GenDB目录下的.sql文件建立Student表。剩下的就不用多说了,Visual FoxPro的程序使用VFP 8.0(可能需要重新设计一下远程视图的连接字符串)。

小心DataAdapter陷阱

1 问题来源

ADO.NET是.net中的主要数据访问方式,在ADO.NET对象模型中,DataAdapter占有举足轻重的作用,它是ADO.NET对象模型中联机和脱机这两部分的桥梁。数据库中的数据通过DataAdapter载入DataSet中,修改后的数据再通过DataAdapter更新回数据库。令人印象深刻的就是DataAdapter对象自身包含四个命令对象:SelectCommand、InsertCommand、DeleteCommand与UpdateCommand,分别掌管数据的检索与更新任务。我们可以对这四个命令对象分别设置CommandText以完成丰富的工作,不用再象ADO时代的RecordSet一样无法控制更新过程。

在我的《并发操作的一致性问题》的前半部分(目前只完成了50%),我用Visual FoxPro为案例说明了如何通过控制WHERE短语的生成策略来完成并发冲突的检测和解决(尽管只能解决部分并发问题)。Visual FoxPro提供了关键字、关键字和可更新字段、关键字和已修改字段、关键字和时间戳四种WHERE短语生成策略,并额外提供两种更新手段:UPDATE更新或先删除后插入更新。

尽管ADO.NET中的DataAdapter理论上也支持这四种方式(请参考David Sceppa著的《ADO.NET技术内幕》一书第10章7小结。博客园idior引用了该书的内容,可以访问《数据库乐观并发处理的策略》一文),但实际上并不是支持的非常好,David Sceppa也建议在DataAdapter中使用主键更新模式或主键+时间戳模式,并推荐使用主键+时间戳模式。然而在我写《并发操作的一致性问题》的后半部分时却发现即便是这两种更新模式仍然存在某些潜在的问题,如果不小心使用将带来意象不到的问题。如此看来,在WHERE短语生成策略上DataAdapter尚差Visual FoxPro好大一截。

2 Visual FoxPro中的更新机制

Visual FoxPro中的更新可用图 1-1来表示:

图 1-1 Visual FoxPro更新机制

在这张图中,我们可以看到Visual FoxPro将字段划分为三类:主键、可更新字段、不可更新字段。显示在图中就是中间画对勾的两列,"钥匙"表示主键、"铅笔"表示可更新列。当然主键也可以同时是可更新字段或不可更新字段。

右侧列出了SQL WHERE短语生成策略以及更新策略。在如图所示的场景中,如果我们选择使用"关键字"作为WHERE短语生成策略,并且只修改了name字段,那么Visual FoxPro究竟会为我们生成什么样的UPDATE命令呢?大家可以自行完成【实验 2-5 用Visual FoxPro验证WHERE短语生成策略】,在SQL Server的事件探察器中探察一番。探察到的命令类似:"exec sp_executesql N'UPDATE dbo.Student SET name=@P1 WHERE id=@P2', N'@P1 varchar(8),@P2 int', '张三*', 1"。值得我们注意的是,在上面生成的SET短语中,只有"name=@P1",尽管我们的可更新字段除了name还有age。

3 DataAdapter的更新机制

再让我们看看DataAdapter的更新策略。为了让DataAdapter在更新时具有同样的"关键字"WHERE短语生成策略,我们在适配器向导中不使用开放式并发,这样生成的UPDATE命令类似:"UPDATE [student] SET [name] = @nameCurrent, [age] = @ageCurrent, [dept] = @deptCurrent WHERE [id] = @idOriginal"

然后我们只在DataSet中修改name字段,而age字段保持不变,修改完后使用DataAdapter命令进行更新,可以在事件探察器中探察到更新命令如下:"exec sp_executesql N'UPDATE [student] SET [name] = @nameCurrent, [age] = @ageCurrent, [dept] = @deptCurrent WHERE [id] = @idOriginal; SELECT * FROM student WHERE [id] = @idCurrent', N'@nameCurrent varchar(8),@ageCurrent int,@deptCurrent varchar(10),@idOriginal int,@idCurrent int', @nameCurrent = '张三*', @ageCurrent = 20, @deptCurrent = '经管系', @idOriginal = 1, @idCurrent = 1"。注意观察SET短语中除了包含修改了的name字段外,连id、age、dept都一并向数据库提交并覆盖现有值。这是因为究竟更新哪几个字段在设计时就由DataAdapter的UpdateCommand命令决定了,因此不管你修改的是一个还是两个字段,其它字段都要一并进行更新(当然,还有更变态的行为,在本文后面要进一步论述)。

其实这种更新策略在Visual FoxPro中也提供了对等的更新手段,那就是"先删除再插入"的方式。而Visual FoxPro中"关键字和已修改字段"的WHERE短语生成策略在DataAdapter中就显得无能为力了(在另外一篇文章《使用DataAdapter实现KeyAndModifiedField更新》中,我会给出一个让DataAdapter实现"关键字和已修改字段"更新策略的例子)。

4 DataAdapter的更新问题

也许你认为这种更新无所谓,覆盖就覆盖了,但在某些场景下很容易引起并发问题,需要小心处理和对待。假设某表中记录了如下几个字段:用户ID、活期存款、定期存款、信用等级。信用等级字段的值取决于用户的银行存款(包括活期和定期),并有一套复杂的运算机制。当用户的存款发生变化时就要相应修改信用等级。

如果存在并发操作,在DataAdapter的默认更新模式下就很容易出现问题:

1、 两个事务同时读取数据库,读入信用等级5。

2、 第一个事务存入10000元,并更新信用等级为6。

3、 第二个事务取走100元,信用等级不发生改变,保存。

我们可以看到,第二个事务不改变信用等级,但DataAdapter的更新策略决定着要一并将信用等级也写入数据库,此时就会覆盖第一个事务的写入的6(当DataAdapter没有使用开放式并发时才会发生)。

由于目前DataAdapter不支持"关键字和已修改字段"的WHERE短语生成策略,因此为了解决这种问题,不得不使用开放式并发将所有字段是否可能发生冲突检查一个遍。"杀鸡用了牛刀"。本应很简单的更新却凭空增加了负担。

5 变态的DataAdapter

DataAdapter的上述行为会导致了一种变态行为,那就是越权更新。我们假设这样一个场景:DataAdapter的SelectCommand为"SELECT id, name, age, dept FROM Student",更新命令为"UPDATE [student] SET [name] = @nameCurrent, [age] = @ageCurrent WHERE [id] = @idOriginal",注意SELECT命令筛选出了四个字段而更新命令的SET短语中只更新name字段与age字段。现在你使用这个DataAdapter将数据装入DataSet,然后修改一个不可更新字段dept的值,然后更新,哈哈,居然能够看到更新动作!

虽然我们修改了一个不可更新的字段dept,但是在DataSet中,该DataRow被标识为Modified,所以在更新时该记录就被送交DataAdapter,而DataAdapter居然根据这个DataRow生成了Update命令,那就是将DataSet中的值再强行写回数据库,而原因就是因为你修改的一个不可更新的字段!变态!

不过这种变态到也不是一无是处。偶然间发现了博客园的两篇文章,应用DataAdapter的这种"变态"行为可以轻而易举的解决文章中所说的问题。一篇文章是Justin Shen的《关于DataSet的更新...》,另外一篇是sumtec的《DataSet 强行更新的狂想》。

有了DataAdapter这种变态特性,文中所提问题可以迎刃而解,那就是给表添加一虚拟字段(什么都行),然后读入DataSet,但不显示给用户。如果希望强制更新,那么就将DataSet中该虚拟字段的值改一下,DataAdapter就乖乖的强行更新了,而且你也不用担心数据库中的虚拟字段值会发生变化,因为它是"不可更新"的。

 

posted on 2005-08-22 17:05  吕震宇  阅读(4668)  评论(7编辑  收藏  举报