ADO 中的并发编码

来源:互联网 发布:淘宝友情链接怎么弄 编辑:程序博客网 时间:2024/05/04 13:30

ADO 中的并发编码

发布日期: 12/23/2004 | 更新日期: 12/23/2004

Rick Dobson

如果有两个用户试图同时更新相同行,将会发生什么?自从共享数据库问世以来,类似的问题就一直在困扰着开发人员。现在,ADO.NET 通过一种名为“开放式并发”的方法灵活地解决了该问题。Rick Dobson 说明了该方法的工作原理以及如何使应用程序在具有高度可伸缩性的环境中变得更加健壮。

*
本页内容
并发问题概述并发问题概述数据更新并发数据更新并发刷新 DataSet 并重新提交更新刷新 DataSet 并重新提交更新重新提交、刷新或还原重新提交、刷新或还原插入和删除并发插入和删除并发Form2 Load 事件过程Form2 Load 事件过程多用户插入问题多用户插入问题多用户删除问题多用户删除问题小结小结

ADO.NET 是为提高可伸缩性而专门设计的 — 尤其适用于在多个用户需要操纵数据库表中的相同行的情况下的数据操纵。ADO.NET 的一项有助于提高可伸缩性的重要功能是它依赖于开放式并发来实现数据操纵。

使用开放式并发,用户不从准备更新某个行的时刻起锁定该行。行锁定仅在 ADO.NET 提交更改的瞬间应用。因此,在第一个用户向该行提交更改之前,另一个用户可以更改相同行。在这种情况下,第一个用户进行的更新引发 DBConcurrencyException 对象。ADO.NET 提供了有助于解决这类问题的功能。多用户问题还可能影响用 ADO.NET 实现的删除和插入。

本文介绍了在用 ADO.NET 操纵数据期间处理并发问题的编码准则。您将了解导致并发问题的上下文以及如何分别针对更新、插入和删除来处理这些问题。通过评述该主题,将向您提供一组用于处理基本并发问题的核心技能,以及用来增加您处理更复杂并发情形的技能的基础。

并发问题概述

并发成为 ADO.NET 的问题的原因是用户通常间接地对数据库进行更改。ADO.NET 通过 DataAdapter 对象将用户连接到数据库,而该对象又依赖于 Connection 对象。用户不是通过 DataAdapter 将更改直接提交给数据库,而是对本地 DataSet 对象进行更改。DataSet 可能包含一个或多个 DataTable 对象,而每个 DataTable 可能包含多个 DataRow 对象。

DataAdapter 对象充当 DataSet 和数据库之间的双向泵。ADO.NET 解决方案最初可以用 DataAdapter 的 Fill 方法填充 DataSet。在用户修改 DataSet 内部的 DataTable 中的一个或多个 DataRow 对象之后,应用程序可以调用 DataAdapter 的 Update 方法将这些更改传输到数据库。Update 方法通过 DataAdapter 的 UpdateCommand、DeleteCommand 和 InsertCommand 属性操作。这些属性代表包装了 SQL UPDATE、DELETE 和 INSERT 语句(或存储过程调用)的 Command 对象。

当 ADO.NET 应用程序具有多个用户时,每个用户通常都具有对单独 DataSet 的访问权(并且通常具有对单独 DataAdapter 的访问权)。当多个用户能够同时更改 DataSets 和调用 DataAdapter的 Update 方法时,将至少有一个 DataSet 可能变得与数据库不同步。在这种情况下,未同步的 DataSet 的 DataAdapter 不知道向哪些行应用一组更改。当用户调用使用未同步 DataSet 的 DataAdapter 的 Update 方法时,ADO.NET 将为第一个具有 DataSet 和数据库之间的未同步值的行引发 DBConcurrencyException 对象。

有四个可能的 DataRowVersion 枚举值,但其中两个 DataRowVersion 枚举与基本并发问题的关系特别密切:Original 和 Current。Original 枚举在 Fill 方法之后但在对它的列值进行任何更改之前应用于 DataRow。Current 在一个或多个列值改变之后但将这些值提交给数据库之前应用于 DataRow。在用 DataTable 中的 DataRow 的 DataAdapter 成功调用 Update 方法之后,ADO.NET 将 DataRow 的当前值分配给 DataRow 的原始值。

对于准备将更改从 DataSet 传输到数据库的 SQL UPDATE、DELETE 和 INSERT 语句的设计而言,对 DataRow 的 Original 枚举和 Current 枚举的引用非常关键。例如,UPDATE 语句在它的 SET 子句中使用当前值,但该语句的 WHERE 子句必须包含对原始值的引用。通过对原始值的引用,DataAdapter 可以评估 DataSet 是否仍然与数据库同步。

返回页首返回页首

数据更新并发

HCVSConcurrency 项目(它包含在随附的下载文件中)中的 Form1 背后的模块通过模拟两个都可以对数据库中的表进行更改的用户,说明了几个在处理数据修改时有用的编码准则。该窗体使用 SQL Server Northwind 数据库中的 Shippers 表来启用对含有 ShipperID 值 1 的行的 CompanyName 列值的更改。

为了简化示例,我以图形方式创建了 DataAdapter (SqlDataAdapter1),它自动填充 UpdateCommand、DeleteCommand 和 InsertCommand 属性以实现开放式并发。然后,我使用 SqlDataAdpapter1 的上下文菜单生成了三个 Dataset,它们的 Name 属性分别为 DataSet1User11、DataSet1User21 和 DataSet1FromDB1。DataSet1User11 和 DataSet1User21 DataSet 用于用户 1 和用户 2。图 1 显示了 Form1 的设计,并且在窗体下面的组件栏中显示了 ADO.NET 组件。

返回页首返回页首

刷新 DataSet 并重新提交更新

Button1 的 Click 事件过程试图用 TextBox1 的 Text 属性值更新数据库。该过程实现了用户 1 所做的更改。Button2 的 Click 事件过程(用于用户 2)为 TextBox2 执行了同一种任务。第二个过程演示了用于处理开放式并发冲突的更成熟技术。当任一用户试图修改 CompanyName 列值时,Form1 背后的代码就开始修改 DataSet1User11 或 DataSet1User21,并调用 SqlDataAdapter1 的 Update 方法。如果所尝试的修改成功,则代码会清除 CheckBox1,它用于指示开放式并发冲突。如果修改失败,则 ADO.NET 会引发 DBConcurrencyException,并且由一个 Try...Catch...Finally 语句处理该 Exception 对象。

下一个代码片段(来自 Form1 的 Load 事件过程)初始化了用户 1 和用户 2 的 DataSet。两个 Fill 方法调用基于 SqlDataAdapter1 的 SelectCommand 属性,用 Northwind 数据库中的值填充了两个本地 DataSet。SelectCommand 的 SQL 语句为所有行返回了 Shippers 表的全部三个列。该代码片段还调用了 RefreshControls。

SqlDataAdapter1.Fill(DataSet1User11)SqlDataAdapter1.Fill(DataSet1User21)RefreshControls()

RefreshControls 过程(其内容显示在下一个代码块中)是从 Form1 Load 事件过程以及 Button1 和 Button2 的 Click 事件过程中调用的。应用程序通过 RefreshControls 过程填充 DataSet1FromDB1 DataSet。然后,代码在 Label1、TextBox1 和 TextBox2 的分配语句中,使用 DataSet1FromDB1 内部的 Shippers DataTable 的第一个 DataRow 中的 CompanyName 列值。每个 TextBox 控件的 Text 属性的表达式都将 1 或 2 追加到 CompanyName 列值中。请注意,在下面的语法中,您可以使用名称或从零开始的索引编号来引用 DataSet 中的 DataTable 和 DataTable 中的列。

SqlDataAdapter1.Fill(DataSet1FromDB1)Label1.Text = "Current DB value: " & _ DataSet1FromDB1.Tables("Shippers"). _ Rows(0)("CompanyName")TextBox1.Text = DataSet1User11.Tables(0). _ Rows(0)(1) & "1"TextBox2.Text = DataSet1User21.Tables(0). _ Rows(0)(1) & "2"

下面的代码片断选自 Button1 的 Click 事件过程,它说明了一个协调开放式并发冲突的简便方法。正如您可以看到的那样,该语句实际上包含两个 Catch 子句。其中一个用于 DBConcurrencyException 对象,第二个用于其他任何种类的 Exception 对象。良好的编程准则规定使用最终的 Catch 子句来捕获前面的更为具体的 Catch 子句所未捕获的任何 Exception 对象。

Try DataSet1User11.Shippers.Rows(0)(1) = _  DataSet1User11.Shippers.Rows(0)(1) + "1" SqlDataAdapter1.Update(DataSet1User11) CheckBox1.Checked = FalseCatch ex As DBConcurrencyException SqlDataAdapter1.Fill(DataSet1User11) DataSet1User11.Shippers.Rows(0)(1) = _  DataSet1User11.Shippers.Rows(0)(1) + "1" SqlDataAdapter1.Update(DataSet1User11) CheckBox1.Checked = TrueCatch ex As Exception str1 = ex.GetType.ToString & _  ControlChars.CrLf & ex.Message MessageBox.Show(str1, "Error form", _  MessageBoxButtons.OK, MessageBoxIcon.Error)Finally RefreshControls()End Try

Try...Catch...Finally 语句的 Try 子句包含三个语句。首先,代码将“1”追加到 DataSet1User11 的 Shippers DataTable 的第一行中的第二个列的值中。因为所有三个应用程序 DataSet 都是类型化的,所以代码可以根据需要使用 .Tablename 而不是 .Tables(Tablename)。第二个语句调用 SqlDataAdapter1 的 Update 方法。如果该 Update 方法成功,则分配给 DataSet 的新值将更新 Northwind 数据库中的 Shippers 表。控制随后传递给 Try 子句中的第三个语句,该语句将清除 CheckBox1。

如果用户 2 自从 DataSet1User11 上次用数据库值刷新以来更新了 Northwind Shippers 表的第一行,则在试图调用 Try 子句中的 Update 时,将引发 DBConcurrencyException 对象,并且将控制传递给第一个 Catch 子句。该子句中的代码首先用 SqlDataAdapter1 的 Fill 方法刷新 DataSet1User11。接下来,示例执行与 Try 子句中相同的更新,但这一次已知 DataSet 值与数据库同步。第一个 Catch 子句通过向 CheckBox1 分配一个复选标记而结束。

无论 Update 方法成功还是失败,Finally 子句都将运行。Finally 子句的唯一语句调用前面讨论过的 RefreshControls。

返回页首返回页首

重新提交、刷新或还原

在单击 Button1 之后立即单击 Button2 可能生成开放式并发失败,而这会导致引发 DBConcurrencyException 对象。这是因为 DataSet1User21 的首行 CompanyName 列值反映了通过单击 Button1 实现更改之前的数据库。与通过上述代码片段所演示的方法强行解决开放式并发冲突不同,Button2 的 Click 事件过程使用户可以从三种可能的解决方案中进行选择。用户可以用刷新后的 DataSet 重新提交更改,或者放弃更改但仍然刷新 DataSet1User21,或者将数据库还原到 Shippers DataTable 中的第一个 DataRow 的原始值。

应用程序通过 InputBox 向用户呈现这三个选项。图 2 显示了 Form1 在最初单击 Button1 之后的样子。请注意,窗体将当前数据库值报告为 Speedy Express1。Form1 下面的 InputBox 提供了用于解决并发冲突的三个选项。选项选择在 InputBox 的 Title 中进行了简化。这些选项被编号为 1、2 和 3。InputBox 的体显示了三个值:

DataSet1User21 中的 Shippers DataTable 的第一行中第二列的 Original 属性值。

DataTable 中第二列的当前值。

Shippers 表的第一行中的 CompanyName 列的当前 Northwind 数据库值。

Button2 Click 事件过程的 Try...Catch...Finally 语句与 Button1 Click 事件过程中的 Try...Catch...Finally 语句具有完全相同的总体设计。但是,DBConcurrencyException 对象的 Catch 子句是不同的。下面的代码片段显示了通过三种不同的技术来处理并发冲突的 DBConcurrencyException Catch 子句。Catch 子句中的代码被划分为三个部分。第一个部分设置并显示 InputBox。第二个部分是一个 Select Case 语句,该语句带有与三种不同的并发解决方案技术中的每一种技术相对应的单独的 Case 子句。最后一个部分由向 CheckBox1 分配一个复选标记的单个语句组成。下列项目符号中的每一个都总结了 Select Case 语句中的 Case 子句的角色。

与 InputBox 函数值 1 对应的 Case 子句刷新了 DataSet1User21,再次进行更改,然后通过调用 SqlDataAdapter1 的 Update 方法将更改提交给数据库。

当 InputBox 函数返回值 2 时,Select Case 语句只是刷新 DataSet1User21,而不将更改重新提交给数据库。该选择将 DataSet1User21 与数据库同步。

InputBox 函数值 3 为 Shippers DataTable 的第一个 DataRow 中的 CompanyName 值恢复了 DataSet1User21 中的原始值,并将该值分配为 Northwind Shippers 表中相应的列值。该方法的语法将 SQL UPDATE 语句包装在 ADO.NET Command 对象中。该技术在将更改提交给数据库时没有使用 SqlDataAdapter1。

最后的 Case Else 子句捕获了提供给 InputBox 函数的除 1、2 和 3 以外的任何值。

Catch ex As DBConcurrencyException str1 = "Value summary:" & ControlChars.CrLf str1 &= "Original value: " & _  DataSet1User21.Shippers.Rows(0) _  ("CompanyName", DataRowVersion.Original). _  ToString & ControlChars.CrLf str1 &= "Current value: " & _  DataSet1User21.Shippers.Rows(0) _  ("CompanyName", DataRowVersion.Current). _  ToString & ControlChars.CrLf SqlDataAdapter1.Fill(DataSet1FromDB1) str1 &= "Database value: " & _  DataSet1FromDB1.Shippers.Rows(0) _  ("CompanyName", DataRowVersion.Current) str1Return = InputBox(str1, _  "1 re-submit change, 2 abort change, " & _  "3 restore original", "2") Select Case str1Return  Case "1"   SqlDataAdapter1.Fill(DataSet1User21)   DataSet1User21.Shippers.Rows(0)(1) = _    Mid(DataSet1User21.Shippers.Rows(0)(1), 1, _    Len(DataSet1User21.Shippers.Rows(0)(1)) _    - 1) + "2"   SqlDataAdapter1.Update(DataSet1User21)  Case "2"   SqlDataAdapter1.Fill(DataSet1User21)  Case "3"   Dim cmd1 As New SqlClient.SqlCommand   cmd1.Connection = SqlConnection1   cmd1.CommandText = _    "Update Shippers SET CompanyName = '" & _    DataSet1User21.Shippers.Rows(0) _    ("CompanyName", DataRowVersion.Original). _    ToString & "' WHERE ShipperID = " & _    DataSet1User21.Shippers.Rows(0) _    ("ShipperID", DataRowVersion.Current). _    ToString   cmd1.Connection.Open()   cmd1.ExecuteNonQuery()   cmd1.Connection.Close()  Case Else   MessageBox.Show("1,2, or 3 only", _   "Warning message", MessageBoxButtons.OK, _   MessageBoxIcon.Information) End Select CheckBox1.Checked = True
返回页首返回页首

插入和删除并发

并发问题或相关种类的考虑除了适用于更新以外,还适用于插入和删除。但是,这些问题的表现形式和解决办法是不同的(插入和删除互不相同,而且它们与更新也是不同的)。图 3 显示了 HCVSConcurrency 项目中的 Form2 的 Design 视图。该项目包含三个 TextBox 控件,用于指定要添加到新行的列值或者指定要删除的行的主键。这两个用户中的每一个都在窗体中具有一行按钮。第一行供用户 1 插入和删除行,以及用数据库值刷新 DataSet1User11。第二行按钮为用户 2 和 DataSet1User21 启用相同功能。这些按钮下面的 DataGrid 控件显示了 Northwind 数据库中的 Shippers 表中的当前值。

可以将用图形方式创建的 ADO.NET 组件从 Form1 复制到 Form2,以便可以将这些组件用于 Form2。Form2 的 Load 事件过程设计了一个自定义 DataAdapter (dap1),以便于将新行插入到 Northwind Shippers 表中。该自定义 DataAdapter 的代码很有趣,这有两个原因。首先,它说明了用于创建准备与 DataSet 一起使用的 DataAdapter 的常规设计原则。其次,它说明了如何将值分配给带有 IDENTITY 属性设置的列,例如,Shippers 表中的 ShipperID 列。

返回页首返回页首

Form2 Load 事件过程

Form2 背后的代码在模块级别声明了 dap1。一种很常见的情况是需要与在模块级别声明的 ADO.NET 类实例相对应的变量,因为这些实例经常被两个或更多个过程使用。用于指定 dap1 DataAdapter 的代码具有两个部分。开始部分将值分配给 DataAdapter 的核心属性。第二个部分演示了如何指定 DataAdapter 的参数。

以下代码片段向 dap1 属性分配了值以便在 Form2 中使用它。Dap1 的实例化语句重用了以图形方式创建的 SqlConnection1 对象,以便使 DataAdapter 指向 Northwind 数据库,而 SQL 字符串指定了 DataAdapter 所连接到的基表以及该表中的列。Dap1 的 InsertCommand 属性的分配语句包含两个 SQL 语句。通过 SET IDENTITY_INSERT 语句可以向 ShipperID 列(它具有 IDENTITY 属性设置)分配值。INSERT INTO 语句指定了如何将 DataSet 内部的 Shippers DataTable 中的行中的列值传输到 Northwind 数据库中的 Shippers 表。尽管 SelectCommand 属性将 SqlConnection1 分配给 dap1,但是仍然需要同时将 SqlConnection1 分配给 dap1 中的 InsertCommand 的 Connection 成员。

三组分配语句(分别对应于每个参数)为 INSERT INTO SQL 语句中的参数处理参数实例化。需要为 SQL 语句中的每个参数添加一个具有适当名称和数据类型的参数。根据应用程序的设计的不同,您可以使用 Parameter 对象的 SourceVersion 属性来指定使用 DataRow 列值的当前值还是原始值作为参数的值。@ShipperID 参数的分配语句演示了用于分配当前值的语法,但是在该示例的上下文中,并不是绝对需要该语句,因为列的当前值是默认的参数值。

dap1 = New SqlClient.SqlDataAdapter _ ("SELECT ShipperID, CompanyName, Phone " & _ "FROM Shippers", SqlConnection1)dap1.InsertCommand = New SqlClient.SqlCommand _ ("SET IDENTITY_INSERT Shippers ON ")dap1.InsertCommand.CommandText &= _ "INSERT INTO Shippers " & _ "(ShipperID, CompanyName, Phone) " & _ "VALUES (@ShipperID, @CompanyName, @Phone)"dap1.InsertCommand.Connection = SqlConnection1Dim prm1 As SqlClient.SqlParameter = _ dap1.InsertCommand.Parameters.Add _ ("@ShipperID", SqlDbType.Int)prm1.SourceColumn = "ShipperID"prm1.SourceVersion = DataRowVersion.CurrentDim prm2 As SqlClient.SqlParameter = _ dap1.InsertCommand.Parameters.Add _ ("@CompanyName", SqlDbType.NVarChar, 40)prm2.SourceColumn = "CompanyName"Dim prm3 As SqlClient.SqlParameter = _ dap1.InsertCommand.Parameters.Add _ ("@Phone", SqlDbType.NVarChar, 24)prm3.SourceColumn = "Phone"

以下代码片段中的其他 Form2 Load 事件过程语句初始化了 DataSet1User11 和 DataSet1User21 中的 Shippers DataTable。这些 DataTable 存储了用户 1 和用户 2 的 Northwind Shippers 表值。对 PopulateGridFromDB 的调用处理了 DataSet1FromDB1。

dap1.Fill(DataSet1User11, "Shippers")dap1.Fill(DataSet1User21, "Shippers")PopulateGridFromDB()

随后出现的是 PopulateGridFromDB 过程中的代码。它的作用是刷新某个 DataSet 并且将该 DataSet 分配为 DataGrid 控件的 DataSource 属性。当数据操纵任务除了包括更新以外还包括插入和删除时,只是对 DataSet 调用 Fill 方法未必能够获得其他用户进行的所有更改。尽管 Fill 方法自己能够获得更新,但它本身无法恢复其他用户进行的插入和删除。清除 DataTable 并重新填充保留该 DataTable 的 DataSet 确实可以从数据库中获得该表的完整的新副本,以反映自 DataTable 上次刷新以来的任何新行或被丢弃的行。

DataSet1FromDB1.Tables("Shippers").Clear()dap1.Fill(DataSet1FromDB1, "Shippers")DataGrid1.DataSource = _ DataSet1FromDB1.Tables("Shippers")

Form2 的 Load 事件过程的结束代码片段将默认值分配给 TextBox1、TextBox2 和 TextBox3 的 Text 属性。这些分配使该窗体立即做好插入以及删除的准备,而不会改变 Northwind Shippers 表中的原始列值。而且,第一个语句提醒您可以对 ShipperID 列进行分配,例如,向其分配值 4。

TextBox1.Text = "4"TextBox2.Text = "CAB, Inc."TextBox3.Text = "(123) 456-7890"
返回页首返回页首

多用户插入问题

只要您能够保证所有新行插入都是唯一的,则在应用程序试图向数据库中插入新行时,就不会出现并发问题。但是,并非每个允许执行插入的应用程序都能够做出这一保证。例如,该部分中的示例插入代码允许输入 IDENTITY 属性值。因为应用程序模拟了两个用户,所以每个用户都可能试图输入带有相同 IDENTITY 属性值的行。即使两个用户没有指定重复的 IDENTITY 属性值,单个用户也可能为在 DataTable 中具有唯一约束的列输入重复值。在试图向 DataTable 添加的 DataRow 带有与另一个 DataRow 的列值相匹配的列值时,如果该列具有唯一约束,则可能引发 ConstraintException 对象。当设计代码以允许插入新行时,应当考虑这两种类型的错误。

Button1 的 Click 事件过程包含两个顺序 Try...Catch 语句 — 每个类型的错误对应一个 Try...Catch 语句。在启动错误捕获之前,代码创建了一行新的列值,这些列具有与 DataSet1User11 的 Shippers DataTable 中的列相同的设计。然后,代码用位于 Form2 顶部的 TextBox 控件的值填充该分离行的列。

Dim drw1 As DataRow = _ DataSet1User11.Tables("Shippers").NewRowdrw1("ShipperID") = Integer.Parse(TextBox1.Text)drw1("CompanyName") = TextBox2.Textdrw1("Phone") = TextBox3.Text

第一个 Try...Catch 语句包含对 DataSet1User11 中的 Shippers DataTable 的 DataRowCollection 成员的 Add 方法的调用。该 Add 方法试图添加先前指定和填充的新行。DataTable 中的 DataRow 列具有类型,并且可能具有约束。如果不满足这些条件,则可能导致引发 Exception(例如,ConstraintException)。第一个 Try...Catch 语句将检测此类以及其他 Exception 对象。

Try DataSet1User11.Tables("Shippers"). _  Rows.Add(drw1)Catch ex As Exception str1 = ex.GetType.ToString & _  ControlChars.CrLf & ex.Message MessageBox.Show(str1, "Error form", _  MessageBoxButtons.OK, MessageBoxIcon.Error)End Try

第二个 Try...Catch 语句检测由主键冲突产生的 SqlException 对象。对于 Shippers 表而言,由两个不同的用户向 ShipperID 列分配重复值(例如,指定 ShipperID 值 4),将导致 ADO.NET 引发该类型的 SqlException 对象。您可以使用 Catch 语句的 When 子句来评估 SqlException 表示哪个 SQL Server 错误。When 子句的参数使用 InStr 函数在 SqlException 类返回的 Message 属性值中搜索表示主键冲突的文本。

Try dap1.Update(DataSet1User11, "Shippers") PopulateGridFromDB()Catch ex As SqlClient.SqlException When InStr _ (ex.Message, "Violation of PRIMARY KEY") > 0 HandlePKViolation _    (DataSet1User11, drw1("ShipperID"))Catch ex As Exception str1 = ex.GetType.ToString & _  ControlChars.CrLf & ex.Message MessageBox.Show(str1, "Error form", _  MessageBoxButtons.OK, MessageBoxIcon.Error)End Try

上述代码片段在检测带有有关主键冲突的消息的 SqlException 对象时调用 HandlePKViolation 过程。正如您可以从以下代码片段中看到的那样,HandlePKViolation 过程执行两个任务。首先,它显示一个消息框,以标识该问题并推荐两个解决方案之一。其次,它操纵本地 DataSet(它用一个名为 das 的变量表示)中的 Shippers DataTable,以查找和移除具有重复主键值的行。

str1 = _ "Primary key already in database table.  " & _ "Modify primary key and re-submit or " & _ "abort attempt to insert."MessageBox.Show(str1, "Error form", _ MessageBoxButtons.OK, MessageBoxIcon.Error)drw1 = das.Tables("Shippers"). _ Rows.Find(PKValue)das.Tables("Shippers").Rows.Remove(drw1)

图 4 显示了 Form2 以及有关主键冲突的错误信息。该窗体在它的 DataGrid 控件中显示,Shippers 表包含一个带有 ShipperID 值 4 的行。该行是通过单击 Button1 (User 1 Insert) 输入的,图中显示了 TextBox 控件内容。单击 Button2 (User 2 Insert) 会引发由主键冲突导致的 SqlException 对象。上述代码片段捕获错误信息并显示消息框(它出现在图 4 的底部)。

Button2 Click 事件过程的代码与 Button1 Click 事件过程的代码具有相同的常规设计。最显著的差异是代码操纵 DataSet1User21 而不是 DataSet1User11。HandlePKViolation 过程专门用来适应两个具有不同名称的 DataSet 中的一个。这种过程的更常规设计可以同时适应一个可变的 DataTable 名称以及一个 DataSet 名称。

返回页首返回页首

多用户删除问题

当试图通过 DataSet 的 DataTable 中的 DataRow 从数据库中的表中删除行时,可能遇到 DBConcurrencyException。当另外一个用户自从您的本地 DataSet 上次刷新以来已经预先删除了相同行时,会发生这种情况。该上下文中的并发错误的一个特别简单的解决方案是,清除该 DataSet 并根据数据库重新填充它。该操作可将本地 DataSet 与数据库同步。

与插入任务一样,在试图从 DataTable 中删除 DataRow 时,还可能生成另一种本地错误。这第二个错误在您试图删除已经不再存在的行时发生。常见的做法是在 DataTable 中搜索目标 DataRow,以避免该类型的错误。如果该搜索发现匹配项,则应用程序可以安全地删除 DataRow。否则,应用程序可以绕过对 Delete 方法的调用,因为目标 DataRow 不存在。

Button4 Click 事件过程中的以下代码片段显示了用于仅当 DataRow 存在时调用 Delete 方法的逻辑。该代码片段用于用户 2;Button3 的 Click 事件过程具有用于用户 1 的类似代码。第一行将 Find 方法的返回值分配给 DataSet1User21 中 Shippers DataTable 的 DataRowCollection 中 TextBox1 的 Text 属性。如果 Find 方法发现某个 DataRow 具有与该 Text 属性值的数值等效值相等的主键,则 drw1 变量不为空。否则,drw1(它具有 DataRow 类型)是 Nothing。如果 drw1 不是 Nothing,则 If 块调用 DataRow 的 Delete 方法。如果 drw1 是 Nothing,则 Else 块只是退出该过程。

drw1 = DataSet1User21.Tables("Shippers"). _ Rows.Find(Integer.Parse(TextBox1.Text))If Not (drw1 Is Nothing) Then drw1.Delete()Else Exit SubEnd If

当代码调用 DataRow 的 Delete 方法时,ADO.NET 不会从 DataTable 中移除 DataRow。相反,DataRow 只是被标记以便删除。当代码下一次调用将 DataTable 的 DataSet 容器与被标记以便删除的 DataRow 连接的 DataAdapter 的 Update 方法时,该 DataAdapter 将尝试移除数据库中的相应行。如果尝试成功,则 DataAdapter 接受该 DataSet 中的更改。如果该尝试由于某种原因(例如,DBConcurrencyException 对象)而失败,则 DataRow 将保留在 DataTable 中。Button4 Click 事件过程中的以下代码片段显示了用于在调用 SqlDataAdapter1 的 Update 方法之后测试 DBConcurrencyException 对象的语法。如果发生异常,则该代码首先清除 DataSet,然后用 Fill 方法重新填充 DataSet1User21。

Try SqlDataAdapter1.Update(DataSet1User21, _  "Shippers") PopulateGridFromDB()Catch ex As DBConcurrencyException DataSet1User21.Clear() dap1.Fill(DataSet1User21, "Shippers")

在结束本部分之前,我希望对 Delete 和 Remove 方法进行简单的对比。Delete 方法只是标记 DataRow 以便 DataAdapter 将其移除。Remove 方法立即将 DataRow 从 DataTable 的 DataRowCollection 中取出。

返回页首返回页首

小结

ADO.NET 通过开放式并发将本地 DataSet 与数据库松散地连接在一起。该设计功能提供了超越 ADO(它通常通过保守式并发操作)的巨大的可伸缩性优点。尽管 ADO.NET 包含一组丰富的功能以简化可能由开放式并发产生的错误的处理,但您仍然需要掌握一些基本技术。本文介绍了一系列代码示例,它们演示了您在处理并发错误(也称为 DBConcurrencyException 对象)时可能觉得有用的特选技术的基础知识。下载文件中提供的 HCVSConcurrency 项目包含本文描述的所有技术以及由于篇幅所限而未能进行描述的其他技术的有效版本。

下载 409DOBSON.ZIP

有关 Hardcore Visual Studio 和 Pinnacle Publishing 的详细信息,请访问它们的位于 http://www.pinpub.com/ 的 Web 站点。

注:这不是 Microsoft Corporation 的 Web 站点。Microsoft 对它的内容不承担责任。

本文是从 Hardcore Visual Studio 2004 年 9 月刊转载的。版权所有 2004,Pinnacle Publishing, Inc.(除非另行说明)。保留所有权利。Hardcore Visual Studio 是 Pinnacle Publishing, Inc. 独立发行的产品。未经 Pinnacle Publishing, Inc. 事先同意,不得以任何形式使用或复制本文的任何部分(评论文章中的简短引用除外)。要联系 Pinnacle Publishing, Inc.,请致电 1-800-788-1900。

转到原英文页面


返回页首返回页首
原创粉丝点击