在多用户环境中,有两种用于更新数据库中数据的模型:开放式并发和保守式并发。设计 DataSet 对象的目的是为了促进将开放式并发用于长时间运行的活动,例如当您对数据进行远程处理以及当用户与数据进行交互时。

保守式并发涉及到锁定数据源中的行,以防止用户因修改数据而影响其他用户。在保守式模型中,当用户执行会应用锁的操作时,其他用户将无法执行可能与锁发生冲突的操作,直到锁所有者释放锁为止。此模型主要用于以下环境:对数据存在激烈争用;用锁保护数据的成本小于在发生并发冲突时回滚事务的成本。

因此,在保守式并发模型中,如果用户在读取某行时有将其更改的意图,他将建立一个锁。在该用户完成更新并释放锁之前,其他任何用户都无法更改锁定行。因此,如果锁定时间将会比较短(例如在以编程方式处理记录时)时,最好实现保守式并发。当用户与数据进行交互时,保守式并发并不是可伸缩的选项,它会使记录被锁定相对长的时间。

对比之下,使用开放式并发的用户在读取行时不会锁定该行。当用户要更新某行时,应用程序必须确定自读取该行以来,其他用户是否更改了该行。开放式并发通常用于对数据争用较小的环境。由于不需要锁定任何记录,它将会提高性能,因为锁定记录需要附加的服务器资源。另外,为了维护记录锁,需要与数据库服务器保持持久连接。由于在开放式并发模型中并不会这样,所以与服务器的连接可以在较少的时间内为大量的客户端提供服务。

在开放式并发模型中,如果当某用户接收到来自数据库的值后,另一用户在该用户试图修改该值之前即将其修改,则认为发生了冲突。

以下各表是根据一个开放式并发示例生成的。

下午 1:00,用户 1 从具有以下值的数据库中读取一行:

CustID LastName FirstName

101 Smith Bob

列名称 初始值 当前值 数据库中的值
CustID 101 101 101
LastName Smith Smith Smith
FirstName Bob Bob Bob

下午 1:01,用户 2 读取同一行。

下午 1:03,用户 2 将 FirstName 从“Bob”更改为“Robert”并更新数据库。

列名称 初始值 当前值 数据库中的值
CustID 101 101 101
LastName Smith Smith Smith
FirstName Bob Robert Bob

由于更新时数据库中的值匹配用户 2 具有的初始值,因此更新成功。

下午 1:05,用户 1 将 Bob 的名更改为“James”并试图更新该行。

列名称 初始值 当前值 数据库中的值
CustID 101 101 101
LastName Smith Smith Smith
FirstName Bob James Robert

此时,由于数据库中的值不再匹配用户 1 所预期的初始值,因此用户 1 遇到了开放式并发冲突。现在,需要决定是用用户 1 提供的更改来改写用户 2 提供的更改还是取消用户 1 的更改。

测试是否存在开放式并发冲突

测试是否存在开放式并发冲突的方法有若干种。其中一种涉及到在表中包含时间戳列。数据库通常会提供时间戳功能,该功能可用于标识记录最后更新的日期和时间。当使用这种方法时,将在表定义中包含时间戳列。每当更新记录时,时间戳都将得到更新,以反映当前的日期和时间。在测试是否存在开放式并发冲突时,对表内容的任何查询都会返回时间戳列。当试图执行更新时,数据库中的时间戳值将与所修改行中包含的初始时间戳值进行比较。如果两者匹配,则会执行更新,并用当前时间更新时间戳列以反映更新。如果两者不匹配,则发生了开放式并发冲突。

测试是否存在开放式并发冲突的另一种方法是验证某行中的所有初始列值是否仍匹配数据库中的相应值。例如,考虑以下查询:

SELECT Col1, Col2, Col3 FROM Table1

若要在更新 Table1 中的某行时测试是否存在开放式并发冲突,请发出以下 UPDATE 语句:

UPDATE Table1 Set Col1 = @NewCol1Value,
              Set Col2 = @NewCol2Value,
              Set Col3 = @NewCol3Value
WHERE Col1 = @OldCol1Value AND
      Col2 = @OldCol2Value AND
      Col3 = @OldCol3Value

只要初始值匹配数据库中的值,就会执行更新。如果已修改某个值,由于 WHERE 子句找不到匹配项,更新将不会修改该行。

请注意,建议始终在查询中返回唯一的主键值。否则,以上 UPDATE 语句会更新多个行,这可能会有悖于您的意图。

如果数据源中的列允许空值,则可能需要扩展 WHERE 子句,以查找本地表和数据源中的匹配空引用。例如,以下 UPDATE 语句验证本地行中的空引用仍匹配数据源中的空引用,或者本地行中的值匹配数据源中的值。

UPDATE Table1 Set Col1 = @NewVal1
  WHERE (@OldVal1 IS NULL AND Col1 IS NULL) OR Col1 = @OldVal1

当使用开放式并发模型时,也可以选择应用限制较少的条件。例如,如果只在 WHERE 子句中使用主键列,那么无论自最后一次查询以来是否已更新其他列,数据都将被改写。也可以只将 WHERE 子句应用于特定列,除非自最后一次查询特定字段以来已将其更新,否则数据也会被改写。

DataAdapter.RowUpdated 事件

DataAdapter.RowUpdated 事件可以与上述方法一起用来向应用程序提供有关开放式并发冲突的通知。在每次尝试从 DataSet 中更新 Modified 行之后,将发生 RowUpdated。它使您能够添加特殊的处理代码,包括在发生异常时进行处理,添加自定义错误信息,添加重试逻辑等。RowUpdatedEventArgs 对象为表中已修改的行返回 RecordsAffected 属性,该属性包含特定更新命令所影响的列数。通过设置更新命令来测试是否存在开放式并发,当开放式并发冲突已发生时,因为没有更新任何记录,所以 RecordsAffected 属性最终将返回值 0。如果是这种情况,则将引发异常。RowUpdated 事件使您能够通过设置合适的 RowUpdatedEventArgs.Status 值(如 UpdateStatus.SkipCurrentRow)来处理这种情况并避免异常。有关 RowUpdated 事件的更多信息,请参阅使用 DataAdapter 事件

或者,可以在调用 Update 之前将 DataAdapter.ContinueUpdateOnError 设置为 true,并在完成 Update 时响应存储在特定行的 RowError 属性中的错误信息。有关更多信息,请参阅添加和读取行错误信息

开放式并发示例

下面是一个简单的示例,它设置一个 DataAdapterUpdateCommand 来测试是否存在开放式并发冲突,然后使用 RowUpdated 事件来测试是否存在开放式并发冲突。当遇到开放式并发冲突时,应用程序将设置为其发出更新命令的行的 RowError,以反映开放式并发冲突。

请注意,传递给 UPDATE 命令的 WHERE 子句的参数值映射到其相应列的 Original 值。

  Dim nwindConn As SqlConnection = New SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind")

  Dim custDA As SqlDataAdapter = New SqlDataAdapter("SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID", nwindConn)

  ' The Update command checks for optimistic concurrency violations in the WHERE clause.
  custDA.UpdateCommand = New SqlCommand("UPDATE Customers (CustomerID, CompanyName) VALUES(@CustomerID, @CompanyName) " & _
                                        "WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", nwindConn)
  custDA.UpdateCommand.Parameters.Add("@CustomerID", SqlDbType.NChar, 5, "CustomerID")
  custDA.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30, "CompanyName")

  ' Pass the original values to the WHERE clause parameters.
  Dim myParm As SqlParameter
  myParm = custDA.UpdateCommand.Parameters.Add("@oldCustomerID", SqlDbType.NChar, 5, "CustomerID")
  myParm.SourceVersion = DataRowVersion.Original
  myParm = custDA.UpdateCommand.Parameters.Add("@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName")
  myParm.SourceVersion = DataRowVersion.Original

  ' Add the RowUpdated event handler.
  AddHandler custDA.RowUpdated, New SqlRowUpdatedEventHandler(AddressOf OnRowUpdated)

  Dim custDS As DataSet = New DataSet()
  custDA.Fill(custDS, "Customers")

  ' Modify the DataSet contents.

  custDA.Update(custDS, "Customers")

  Dim myRow As DataRow

  For Each myRow In custDS.Tables("Customers").Rows
    If myRow.HasErrors Then Console.WriteLine(myRow(0) & vbCrLf & myRow.RowError)
  Next


Private Shared Sub OnRowUpdated(sender As object, args As SqlRowUpdatedEventArgs)
  If args.RecordsAffected = 0
    args.Row.RowError = "Optimistic Concurrency Violation Encountered"
    args.Status = UpdateStatus.SkipCurrentRow
  End If
End Sub

  SqlConnection nwindConn = new SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind");

  SqlDataAdapter custDA = new SqlDataAdapter("SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID", nwindConn);

  // The Update command checks for optimistic concurrency violations in the WHERE clause.
  custDA.UpdateCommand = new SqlCommand("UPDATE Customers (CustomerID, CompanyName) VALUES(@CustomerID, @CompanyName) " +
                                        "WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", nwindConn);
  custDA.UpdateCommand.Parameters.Add("@CustomerID", SqlDbType.NChar, 5, "CustomerID");
  custDA.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30, "CompanyName");

  // Pass the original values to the WHERE clause parameters.
  SqlParameter myParm;
  myParm = custDA.UpdateCommand.Parameters.Add("@oldCustomerID", SqlDbType.NChar, 5, "CustomerID");
  myParm.SourceVersion = DataRowVersion.Original;
  myParm = custDA.UpdateCommand.Parameters.Add("@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName");
  myParm.SourceVersion = DataRowVersion.Original;

  // Add the RowUpdated event handler.
  custDA.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated);

  DataSet custDS = new DataSet();
  custDA.Fill(custDS, "Customers");

  // Modify the DataSet contents.

  custDA.Update(custDS, "Customers");

  foreach (DataRow myRow in custDS.Tables["Customers"].Rows)
  {
    if (myRow.HasErrors)
      Console.WriteLine(myRow[0] + "\n" + myRow.RowError);
  }



protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args)
{
  if (args.RecordsAffected == 0) 
  {
    args.Row.RowError = "Optimistic Concurrency Violation Encountered";
    args.Status = UpdateStatus.SkipCurrentRow;
  }
}

为了避免在编辑 DataTable 中的值时每次发生行错误都必须响应,可将错误信息添至行中,以便以后使用。DataRow 对象通过对各行提供 RowError 属性来支持此功能。将数据添至 DataRowRowError 属性会将 DataRowHasErrors 属性标记为 true。如果 DataRowDataTable 的组成部分,且 DataRow.HasErrorstrue,则 DataTable.HasErrors 属性也是 true。这也适用于 DataTable 所属的 DataSet。为错误做测试时,可以检查 HasErrors 属性以确定错误信息是否已添至所有行。如果 HasErrorstrue,则可使用 DataTableGetErrors 方法以便只返回和检查有错误的行,如下例所示。

[Visual Basic]
Dim workTable As DataTable = New DataTable("Customers")
workTable.Columns.Add("CustID", Type.GetType("System.Int32"))
workTable.Columns.Add("Total", Type.GetType("System.Double"))

AddHandler workTable.RowChanged, New DataRowChangeEventHandler(AddressOf OnRowChanged)

Dim I As Int32

For I = 0 To 10
  workTable.Rows.Add(New Object() {I, I*100})
Next

If workTable.HasErrors Then
  Console.WriteLine("Errors In Table " & workTable.TableName)

  Dim myRow As DataRow

  For Each myRow In workTable.GetErrors()
    Console.WriteLine("CustID = " & myRow("CustID").ToString())
    Console.WriteLine(" Error = " & myRow.RowError & vbCrLf)
  Next
End If

Private Shared Sub OnRowChanged(sender As Object, args As DataRowChangeEventArgs)
  ' Check for zero values.
  If CDbl(args.Row("Total")) = 0 Then args.Row.RowError = "Total cannot be 0."
End Sub

[C#]
DataTable  workTable = new DataTable("Customers");
workTable.Columns.Add("CustID", typeof(Int32));
workTable.Columns.Add("Total", typeof(Double));

workTable.RowChanged += new DataRowChangeEventHandler(OnRowChanged);

for (int i = 0; i < 10; i++)
  workTable.Rows.Add(new Object[] {i, i*100});

if (workTable.HasErrors)
{
  Console.WriteLine("Errors In Table " + workTable.TableName);

  foreach (DataRow myRow in workTable.GetErrors())
  {
    Console.WriteLine("CustID = " + myRow["CustID"]);
    Console.WriteLine(" Error = " + myRow.RowError + "\n");
  }
}

protected static void OnRowChanged(Object sender, DataRowChangeEventArgs args)
{
  // Check for zero values.
  if (args.Row["Total"].Equals(0D))
    args.Row.RowError = "Total cannot be 0.";
}