性能的最优化是有代价的。因为数据集对象与下层数据库断开,经常有机会出现数据没有超期(out of date)的情况。因为数据集不保存活动数据,只保存当时填充数据集的活动数据的一个快照,与数据并发性相关的问题就会出现。数据并发性问题出现在多个用户访问相同的数据并且任何一个用户没有其它用户的信息就能更新数据。这就出现了一个用户偶然更新数据而不知道那些数据已经改变了,不是他在程序中看到的了。幸运的是数据集对象拥有捕捉数据并发性问题的内建(built-in)支持,因此应用程序能正确地作出反应。示例程序
CSR也使用应用程序执行通过电子或者缓慢的邮件发送给公司的请求。这些请求在CSR间均匀分开,在天天早晨发送给他们,CSR通过电话执行那些请求。系统设计要提高请求的实现速度,所有的顾客在CSR之间共享。顾客的每个请求,无论通过电话或者邮件,都被不同的CSR处理,增加了发生数据并发性问题的机会。 为了提高性能,应用程序在内存中保持了一个用顾客和订单信息填充的数据集对象。因为很多雇员同时使用该应用程序,就会有许多活动数据的不连接的快照,它们都在雇员的工作站上。所有顾客的维护、订单输入和订单维护都使用名为dsAllData的数据集对象。图1是建立dsAllData的代码,它是全局模块的一部分,因此应用程序中的所有窗体都能使用它。 Const connString = "server=localhost;database=northwind;uid=sa;pwd=" Public connCustSvc As SqlClient.SqlConnection Public daCustomer As SqlClient.SqlDataAdapter Public cbCustomer As SqlClient.SqlCommandBuilder Public daOrders As SqlClient.SqlDataAdapter Public cbOrders As SqlClient.SqlCommandBuilder Public daOrderDetail As SqlClient.SqlDataAdapter Public cbOrderDetail As SqlClient.SqlCommandBuilder Public dsAllData As DataSet Public Sub Main() connCustSvc = New SqlClient.SqlConnection(connString) daCustomer = New SqlClient.SqlDataAdapter("SELECT * FROM Customer", connCustSvc) cbCustomer = New SqlClient.SqlCommandBuilder(daCustomer) daOrders = New SqlClient.SqlDataAdapter("SELECT * FROM Orders", connCustSvc) cbOrders = New SqlClient.SqlCommandBuilder(daOrders) daOrderDetail = New SqlClient.SqlDataAdapter("SELECT * FROM OrderDetail", connCustSvc) cbOrderDetail = New SqlClient.SqlCommandBuilder(daOrderDetail) dsAllData = New DataSet() daCustomer.MissingSchemaAction = MissingSchemaAction.AddWithKey daCustomer.Fill(dsAllData, "Customer") daOrders.MissingSchemaAction = MissingSchemaAction.AddWithKey daOrders.Fill(dsAllData, "Orders") dsAllData.Tables("Orders").Columns("Total").DefaultValue = 0 daOrderDetail.MissingSchemaAction = MissingSchemaAction.AddWithKey daOrderDetail.Fill(dsAllData, "OrderDetail") application.Run(New frmCustomerMaintenance()) End Sub 图1.填充数据集对象
为了输入订单,使用图3中的代码建立了几个新行并添加到dsAllData中。首先建立一个Order记录,接着在数据表OrderDetail中为订单的每个项建立几个记录。当所有必须的行添加到dsAllData后,一个适当的数据适配器的Update方法调用将用新行更新下层数据源。 PRivate Sub CreateOrder() Dim dr As DataRow dr = dsAllData.Tables("Orders").NewRow With dr .Item("DateOrdered") = Now .Item("CustomerID") = 1 .Item("ShipToAddress") = "123 Main" .Item("ShipToCity") = "Kansas City" .Item("ShipToState") = "MO" .Item("ShipToZip") = "12345" End With dsAllData.Tables("Orders").Rows.Add(dr) AddOrderDetail(dr.Item("ID"), 1, 1, 9.99) AddOrderDetail(dr.Item("ID"), 2, 2, 4.99) daOrders.Update(dsAllData.Tables("Orders")) daOrderDetail.Update(dsAllData.Tables("OrderDetail")) End SubPrivate Sub AddOrderDetail(ByVal OrderID As Integer, _ ByVal ProdUCtID As Integer, ByVal Quantity As Integer, _ ByVal Price As Single) Dim dr As DataRow dr = dsAllData.Tables("OrderDetail").NewRow With dr .Item("OrderID") = OrderID .Item("ProductID") = ProductID .Item("Quantity") = Quantity .Item("Price") = Price End With dsAllData.Tables("OrderDetail").Rows.Add(dr) End Sub图3.有细节记录的新订单
该应用程序的数据模型很简单(图4)。它使用SQL Server 2000存储,只包含三个表,顾客表、订单表、每个订单的细节表,并定义了适当的主键和关系以确保参照的完整性。此外,在OrderDetail上定义了一个触发器来更新Orders 表的Total列。每次插入、更新或者删除一个OrderDetail记录,调用触发器计算该订单的最后销售值,并更新Orders表的适当的行。图5是trg_UpdateOrderTotal触发器的代码: CREATE TRIGGER trg_UpdateOrderTotal ON [dbo].[OrderDetail] FOR INSERT, UPDATE, DELETE AS DECLARE @OrderID int SELECT @OrderID=OrderID FROM Inserted IF @OrderID IS NULL SELECT @OrderID=OrderID FROM Deleted UPDATE Orders SET Total= ( SELECT Sum(Price*Quantity) FROM OrderDetail WHERE OrderID=@OrderID ) WHERE ID=@OrderID 图5.更新Total列第一个数据并发性异常
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异常而不是执行更新。
在决定下层数据中的哪些行需要更新后,数据适配器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语句使用参数而不是实际值,但是你能看到行中每列是怎样检查的。
Try daCustomer.Update(dsAllData.Tables("Customer")) Catch dbcEx As Data.DBConcurrencyException Dim dResult As DialogResult dResult = MessageBox.Show(messageString, _ "Data Concurrency Exception Occurred", _ MessageBoxButtons.YesNoCancel, MessageBoxIcon.Error, _ MessageBoxDefaultButton.Button1, _ MessageBoxOptions.DefaultDesktopOnly) If dResult = DialogResult.Yes Then '两个选择:填充整个表或者刷新该行 'daCustomer.Fill(dsAllData.Tables("Customer")) UpdateRow("Customer", dbcEx.Row.Item("ID")) ElseIf dResult = DialogResult.No Then '保存新行的拷贝 Dim drCopy As DataRow, drCurrent As DataRow drCopy = dsAllData.Tables("Customer").NewRow() Dim dc As DataColumn drCurrent = dsAllData.Tables("Customer").Rows.Find(dbcEx.Row.Item("ID")) For Each dc In drCurrent.Table.Columns If dc.ReadOnly = False Then drCopy.Item(dc.ColumnName) = drCurrent.Item(dc.ColumnName) Next '从数据库中获取当前值 UpdateRow("Customer", dbcEx.Row.Item("ID")) '现在恢复用户输入的值并再次保存 For Each dc In drCurrent.Table.Columns If dc.ReadOnly = False Then drCurrent.Item(dc.ColumnName) = drCopy.Item(dc.ColumnName) Next daCustomer.Update(dsAllData.Tables("Customer")) End If End Try图7.捕捉并发性异常 图7显示了一个更好的捕捉该异常的方案。Try...Catch块捕捉了DBConcurrencyException并给用户显示一个标识该错误的消息窗口,给用户提供一个选择(图8所示)。这样我识别已经出现了一个并发性错误并有两个选择:我可以检索下层数据并显示给用户,强制他们再次作修改,或者我能简单地使用该用户指定的改变覆盖下层数据。这些选项都显示在消息框中(图8):
图8.处理数据并发性异常 Private Sub UpdateRow(ByVal TableName As String, ByVal ID As Integer) '获取到特定行的引用 Dim dr As DataRow = dsAllData.Tables(TableName).Rows.Find(ID) '建立命令更新获取新的下层数据 Dim cmd As New SqlClient.SqlCommand("SELECT * FROM " & TableName " WHERE ID=" & ID.ToString(), connCustSvc) '打开连接并建立数据读取器(DataReader) connCustSvc.Open() Dim rdr As SqlClient.SqlDataReader = cmd.ExecuteReader() rdr.Read() '将新数据从数据库复制到数据行 Dim dc As DataColumn For Each dc In dr.Table.Columns If dc.ReadOnly = False Then _ dr.Item(dc.ColumnName) = rdr.Item(dc.ColumnName) Next '接受数据行中的改变 dr.AcceptChanges() connCustSvc.Close() End Sub图9. UpdateRow程序更新缓冲的数据行