帮酷LOGO
0 0 评论

介绍

本文解释了事务意外覆盖修改后的数据以及如何防止它发生的问题。

目录

本文是 SQL Server 特定的。 如果使用Oracle环境,请看一下 Oracle Oracle的替代组件。

介绍,问题是。

创建使用数据库的程序时,人们常常认为数据库充分地处理并发性。 这部分是 true,但不是整个故事。

真正发生的是 SQL Server 负责锁定。 换句话说,它把访问同一资源的操作放到了一。 例如,如果试图修改正在修改的行,则将'暂停'放置在。 因为其他会话对行有排行锁,所以发生。 释放锁后,你可以随意更新行。 如果阻塞锁的持续时间很短,你甚至不会注意到这种情况,通常情况下是这样的。

锁定行可以保证修改行的操作不能同时保证,所修改的行仍然包含与执行该行相同的数据。 考虑以下情况

在上述方案中,两个会话都从数据库读取订单行。 开始时,行有 10个订单,并且这两个会话将根据客户的信息更新它。

第一个销售人员接收客户要订购五个项目的信息,这样会话将从 10到 15更新订单。 同时,它的他销售人员收到客户要订购另一个项目的信息,这样行将再次更新,从 10到 11.

由于行被锁定,后一个更新必须等待。 第一个会话成功更新记录,新的订单数量为 15. 现在当事务由第一个会话提交时,第二个会话可以继续更新。 结果,该行被更新,而现在已经排序的金额为 11.

所发生的事情是,第二个会话不了解或者调查该行在从数据库读取后更新了该行。 在事务中,这种情况被称为丢失的更新。

因这里,本文的目的是解释如何使用行版本检查扩展事务范围外的数据版本。

演示程序。

若要测试不同的方案,可以使用附加项目模拟现有行上的更新。 它实现了本文中解释的所有变化。

A few warnings about the demo program: 在演示程序中编写的代码并不像在真实环境中那样被使用。 我故意打破了数据库编程的一些黄金规则。 我保持连接开放,但是程序中最重要的缺陷是我允许在事务中间使用用户对话。 在现实世界中,你永远不应该这样做。 但是,在这个演示程序中,它更容易测试不同的场景。

基本更新插件

这样说,确保更新一个正确的行是简单的,根据唯一的键更新记录。 让我们看看程序中的基本更新。

假设你有如下所示的表

CREATETABLE Concurrency1 (
 Id intidentity (1,1) NOTNULL,
 TextColumn varchar(100) NULL,
 ValueColumn decimalNULL);

你可以将表从表中提取到网格,一旦从网格中选择一行,就可以使用下面的代码更新它。

使用下面的代码完成提取操作

///<summary>/// Fetch the data from the database and return as a collection///</summary>///<returns>Collection of data items</returns>overridepublic ObservableCollection<Data.IData> FetchData() {
 try {
 using (SqlCommand command = new SqlCommand()) {
 command.Connection = this.connection;
 command.CommandText = @"SELECT Id,
 TextColumn,
 ValueColumn
FROM Concurrency1";
 using (SqlDataReader reader = command.ExecuteReader()) {
 this.CurrentDataCollection = new ObservableCollection<Data.IData>();
 while (reader.Read()) {
 this.CurrentDataCollection.Add(new Data.TheData() {
 Id = (int)reader["Id"],
 TextValue = reader["TextColumn"]!= System.DBNull.Value 
? (string)reader["TextColumn"] : null,
 NumberValue = reader["ValueColumn"]!= System.DBNull.Value 
? (decimal?)reader["ValueColumn"] : null });
 }
 }
 }
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnthis.CurrentDataCollection;
 }
 returnthis.CurrentDataCollection;
}
'''<summary>''' Fetch the data from the database And return as a collection'''</summary>'''<returns>Collection of data items</returns>PublicOverridesFunction FetchData() As ObservableCollection(Of Data.TheData)
 Dim data As Data.TheData
 TryUsing command As SqlCommand = New SqlCommand()
 command.Connection = Me.connection
 command.CommandText = "SELECT Id,
 TextColumn,
 ValueColumn
FROM Concurrency1"Using reader As SqlDataReader = command.ExecuteReader()
 Me.CurrentDataCollection = New ObservableCollection(Of Data.TheData)()
 While (reader.Read())
 data = New Data.TheData()
 With data
. Id = DirectCast(reader("Id"), Integer)
. TextValue = If(Not reader.IsDBNull(1), reader("TextColumn").ToString(), Nothing)
. NumberValue = If(Not reader.IsDBNull(2), _
 DirectCast(reader("ValueColumn"), Decimal?), Nothing)
 EndWithMe.CurrentDataCollection.Add(data)
 EndWhileEndUsingEndUsingCatch exception As System.Exception
 System.Windows.MessageBox.Show(exception.Message, _
 exception.Source, _
 System.Windows.MessageBoxButton.OK, _
 System.Windows.MessageBoxImage.Error)
 ReturnMe.CurrentDataCollection
 EndTryReturnMe.CurrentDataCollectionEndFunction

更新操作如下所示

///<summary>/// Saves a single data item///</summary>///<paramname="data">Data to save</param>///<returns>True if succesful</returns>overridepublicbool SaveItem(Data.IData data) {
 int rowsAffected;
 try {
 this.BeginTransaction(); 
 using (SqlCommand command = new SqlCommand()) {
 command.Connection = this.connection;
 command.Transaction = this.transaction;
 command.CommandText = @"UPDATE Concurrency1
SET TextColumn = @TextColumn,
 ValueColumn = @ValueColumn
WHERE Id = @Id";
 command.Parameters.AddWithValue("@TextColumn", data.TextValue);
 command.Parameters.AddWithValue("@ValueColumn", data.NumberValue.HasValue 
? (object)data.NumberValue.Value 
 : System.DBNull.Value);
 command.Parameters.AddWithValue("@Id", data.Id);
 rowsAffected = command.ExecuteNonQuery();
 if (rowsAffected!= 1) {
 thrownew Exception(string.Format("Wrong number of rows ({0}) affected", rowsAffected));
 }
 }
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnfalse;
 }
 returntrue;
}
'''<summary>''' Saves a single data item'''</summary>'''<paramname="data">Data to save</param>'''<returns>True if succesful</returns>PublicOverridesFunction SaveItem(data As Data.TheData) AsBooleanDim rowsAffected AsIntegerTryIf (Me.transaction IsNothing) ThenMe.transaction = Me.connection.BeginTransaction()
 EndIfUsing command As SqlCommand = New SqlCommand()
 command.Connection = Me.connection
 command.Transaction = Me.transaction
 command.CommandText = "UPDATE Concurrency1
SET TextColumn = @TextColumn,
 ValueColumn = @ValueColumn
WHERE Id = @Id" command.Parameters.AddWithValue("@TextColumn", data.TextValue)
 command.Parameters.AddWithValue("@ValueColumn", _
 If(data.NumberValue.HasValue, data.NumberValue.Value, System.DBNull.Value))
 command.Parameters.AddWithValue("@Id", data.Id)
 rowsAffected = command.ExecuteNonQuery()
 If (rowsAffected <> 1) ThenThrowNew System.Exception(String.Format("Wrong number of rows ({0}) affected", _
 rowsAffected))
 EndIfEndUsingCatch exception As System.Exception
 System.Windows.MessageBox.Show(exception.Message, _
 exception.Source, _
 System.Windows.MessageBoxButton.OK, _
 System.Windows.MessageBoxImage.Error)
 ReturnFalseEndTryReturnTrueEndFunction

代码非常简单: 创建命令,设置参数,执行命令,并在结束时检查受影响的行数。 WHERE 子句中的主 key-value 用于确保更新正确的行。

你可以在许多地方找到这种代码。 代码正确的( 只要忽略了连接和事务生存期) 会覆盖数据库中的行,而不检查数据版本的任何内容。

要演示这里问题,请执行以下操作:

  • 打开程序的两个实例
  • 实例 1:
    • 获取数据
    • 选择第一行
  • 实例 2:
    • 获取数据
    • 选择第一行
  • 实例 1:
    • 将数字字段修改为 123
    • 按保存按钮
  • 实例 2:
    • 将数字字段修改为 456
    • 按下保存按钮注意程序冻结,因为它等待锁定
  • 实例 1:
    • 按提交按钮,实例 2更新现在继续
  • 实例 2:
    • 按提交按钮
  • 实例 1:
    • 获取数据
  • 实例 2:
    • 获取数据

当查看网格中的数据时,可以注意到,实例 2中更新的数据仍然是数字字段的456 值。 在实例 1中更新的数据只是被覆盖。

更新时,检查 rowversion。

解决这个问题的方法是确保更新行的正确行和正确版本。 SQL Server 有一个名为 rowversion ( 以前称为 timestamp )的特殊数据类型。 尽管数据类型有 NAME,但它并不包含任何关于时间的信息,它实际上是一个版本号。 这种类型的唯一目的是在行发生更改时自动更改,因此你可以使用这里类型来观察更改。 现在这个表看起来像这样

CREATETABLE Concurrency2 (
 Id intidentity (1,1) NOTNULL, CurrentVersion rowversion NOTNULL, TextColumn varchar(100) NULL,
 ValueColumn decimalNULL);

就像你所看到的,还有一个叫做 CurrentVersion的列。 此列现在存储当前的rowversion。

在更新行之前,要调查的代码行很少。 rowversion的二进制数据 Having 长度为 8字节。 为了更易于观察和处理,我使用了定制的Rowversion类。 它重写ToString方法,使rowversion的值可以很容易地显示在网格中。 类还确保数据的长度精确为 8字节。

///<summary>/// Class to hold the rowversion value///</summary>publicclass Rowversion {
 private byte[] theValue;
 ///<summary>/// The actual byte array///</summary>public byte[] TheValue {
 get {
 returnthis.theValue;
 }
 set {
 if (value == null || value.Length!= 8) {
 thrownew System.Exception("Invalid rowversion value");
 }
 this.theValue = value;
 }
 }
 ///<summary>/// Default constructor///</summary>public Rowversion() {
 this.TheValue = new byte[8];
 }
 ///<summary>/// Rowversion value in hexadecimal format///</summary>///<returns></returns>publicoverridestring ToString() {
 StringBuilder sb = new StringBuilder("0x");
 foreach (byte item inthis.TheValue) {
 sb.Append(item.ToString("X"));
 }
 return sb.ToString();
 }
}
'''<summary>''' Class to hold the rowversion value'''</summary>PublicClass Rowversion
 Private _theValue AsByte()
 '''<summary>''' The actual byte array'''</summary>PublicProperty TheValue() AsByte()
 GetReturnMe._theValue
 EndGetSetIf (Value IsNothingOr Value.Length <> 8) ThenThrowNew System.Exception("Invalid rowversion value")
 EndIfMe._theValue = Value
 EndSetEndProperty'''<summary>''' Default constructor'''</summary>PublicSubNew()
 Me.TheValue = NewByte(7) {}
 EndSub'''<summary>''' Rowversion value in hexadecimal format'''</summary>'''<returns>The row version</returns>PublicOverridesFunction ToString() AsStringDim sb As StringBuilder = New StringBuilder("0x")
 ForEach item AsByteInMe.TheValue
 sb.Append(item.ToString("X"))
 NextReturn sb.ToString()
 EndFunctionEndClass

当获取数据时,代码基本上与以前相同,但是使用 GetBytes 方法检索 rowversion 列的值并放入自定义rowversion类。

///<summary>/// Fetch the data from the database and return as a collection///</summary>///<returns>Collection of data items</returns>overridepublic ObservableCollection<Data.IData> FetchData() {
 Data.TheData data;
 try {
 using (SqlCommand command = new SqlCommand()) {
 command.Connection = this.connection;
 command.CommandText = @"SELECT Id,
 TextColumn,
 ValueColumn, CurrentVersionFROM Concurrency2";
 using (SqlDataReader reader = command.ExecuteReader()) {
 this.CurrentDataCollection = new ObservableCollection<Data.IData>();
 while (reader.Read()) {
 data = new Data.TheData();
 data.Id = (int)reader["Id"];
 data.TextValue = reader["TextColumn"]!= System.DBNull.Value 
? (string)reader["TextColumn"] : null;
 data.NumberValue = reader["ValueColumn"]!= System.DBNull.Value 
? (decimal?)reader["ValueColumn"] : null; data.CurrentRowVersion = new Data.Rowversion();
 reader.GetBytes(3, 0, data.CurrentRowVersion.TheValue, 0, 8);this.CurrentDataCollection.Add(data);
 }
 }
 }
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnthis.CurrentDataCollection;
 }
 returnthis.CurrentDataCollection;
}
'''<summary>''' Fetch the data from the database And return as a collection'''</summary>'''<returns>Collection of data items</returns>PublicOverridesFunction FetchData() As ObservableCollection(Of Data.TheData)
 Dim data As Data.TheData
 TryUsing command As SqlCommand = New SqlCommand()
 command.Connection = Me.connection
 command.CommandText = "SELECT Id,
 TextColumn,
 ValueColumn, CurrentVersionFROM Concurrency2"Using reader As SqlDataReader = command.ExecuteReader()
 Me.CurrentDataCollection = New ObservableCollection(Of Data.TheData)()
 While (reader.Read())
 data = New Data.TheData()
 With data
. Id = DirectCast(reader("Id"), Integer)
. TextValue = If(Not reader.IsDBNull(1), _
 reader("TextColumn").ToString(), Nothing)
. NumberValue = If(Not reader.IsDBNull(2), _
 DirectCast(reader("ValueColumn"), Decimal?), Nothing). CurrentRowVersion = New Data.Rowversion()
 reader.GetBytes(3, 0,. CurrentRowVersion.TheValue, 0, 8)EndWithMe.CurrentDataCollection.Add(data)
 EndWhileEndUsingEndUsingCatch exception As System.Exception
 System.Windows.MessageBox.Show(exception.Message, _
 exception.Source, _
 System.Windows.MessageBoxButton.OK, _
 System.Windows.MessageBoxImage.Error)
 ReturnMe.CurrentDataCollection
 EndTryReturnMe.CurrentDataCollectionEndFunction

重要的是,现在你总是在用户界面中显示的所有行中为程序中的rowversion 提供值。 如果希望更新行,则可以使用如下代码

///<summary>/// Saves a single data item///</summary>///<paramname="data">Data to save</param>///<returns>True if succesful</returns>overridepublicbool SaveItem(Data.IData data) {
 int rowsAffected;
 try {
 this.BeginTransaction();
 using (SqlCommand command = new SqlCommand()) {
 command.Connection = this.connection;
 command.Transaction = this.transaction;
 command.CommandText = @"UPDATE Concurrency2
SET TextColumn = @TextColumn,
 ValueColumn = @ValueColumn
WHERE Id = @IdAND CurrentVersion = @CurrentVersion"; command.Parameters.AddWithValue("@TextColumn", data.TextValue);
 command.Parameters.AddWithValue("@ValueColumn", data.NumberValue.HasValue 
? (object)data.NumberValue.Value : System.DBNull.Value);
 command.Parameters.AddWithValue("@Id", data.Id); command.Parameters.AddWithValue("@CurrentVersion", data.CurrentRowVersion.TheValue); rowsAffected = command.ExecuteNonQuery();if (rowsAffected!= 1) {
 thrownew System.Exception("Row versions do not match.");
 } }
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnfalse;
 }
 returntrue;
 }
'''<summary>''' Saves a single data item'''</summary>'''<paramname="data">Data to save</param>'''<returns>True if succesful</returns>PublicOverridesFunction SaveItem(data As Data.TheData) AsBooleanDim rowsAffected AsIntegerTryIf (Me.transaction IsNothing) ThenMe.transaction = Me.connection.BeginTransaction()
 EndIfUsing command As SqlCommand = New SqlCommand()
 command.Connection = Me.connection
 command.Transaction = Me.transaction
 command.CommandText = "UPDATE Concurrency2
SET TextColumn = @TextColumn,
 ValueColumn = @ValueColumn
WHERE Id = @IdAND CurrentVersion = @CurrentVersion" command.Parameters.AddWithValue("@TextColumn", data.TextValue)
 command.Parameters.AddWithValue("@ValueColumn", If(data.NumberValue.HasValue, _
 data.NumberValue.Value, System.DBNull.Value))
 command.Parameters.AddWithValue("@Id", data.Id) command.Parameters.AddWithValue("@CurrentVersion", data.CurrentRowVersion.TheValue) rowsAffected = command.ExecuteNonQuery()If (rowsAffected <> 1) ThenThrowNew System.Exception("Row versions do not match.")
 EndIfEndUsingCatch exception As System.Exception
 System.Windows.MessageBox.Show(exception.Message, _
 exception.Source, _
 System.Windows.MessageBoxButton.OK, _
 System.Windows.MessageBoxImage.Error)
 ReturnFalseEndTryReturnTrueEndFunction

代码与前一个非常相似,但在本文中,你还可以检查 CurrentRowversion 列是否与数据库中的记录相同。

如果基础在数据库中已经更改,则不会从语句中得到错误。 因为这种情况下,语句是完全有效的,所以它不更新任何行。 因此,调查正确的行数量是非常关键的。 如果更新单行,则更新应只影响一行和多行。

如果你在第一个示例中使用这里代码版本重复相同的测试,就会得到像下面这样的错误消息。

当你再次将数据提取到网格并重复修改后,该程序就会成功保存,因为现在程序知道当前的rowversion。

在存储过程中进行检查。

许多人使用存储过程来更改数据。 这种情况下,原则是相同的,但是你只需要将rowversion的当前值传递给程序,以便进行检查。 过程可以如下所示

-------------------------------------------- Procedure for saving to concurrency 2------------------------------------------IFOBJECT_ID ( 'procConcurrency2', 'P' ) ISNOTNULL 
 DROPPROCEDURE procConcurrency2; GOCREATEPROCEDURE procConcurrency2(
 @Idint,
 @CurrentVersion rowversion,
 @TextColumnvarchar(100),
 @ValueColumndecimal) ASBEGINUPDATE Concurrency2
 SET TextColumn = @TextColumn,
 ValueColumn = @ValueColumnWHERE Id = @IdAND CurrentVersion = @CurrentVersion;
 IF@@ROWCOUNT!= 1BEGINRAISERROR('Row versions do not match.', 16, 1);
 ENDEND;GO

更新行的语句与前面的示例相同,因这里检查更新了多少行。 使用这里过程,调用方可以如下所示

///<summary>/// Saves a single data item///</summary>///<paramname="data">Data to save</param>///<returns>True if succesful</returns>overridepublicbool SaveItem(Data.IData data) {
 int rowsAffected;
 try {
 this.BeginTransaction();
 using (SqlCommand command = new SqlCommand()) {
 command.Connection = this.connection;
 command.Transaction = this.transaction;
 command.CommandType = System.Data.CommandType.StoredProcedure;
 command.CommandText = @"procConcurrency2";
 command.Parameters.AddWithValue("@Id", data.Id);
 command.Parameters.AddWithValue("@CurrentVersion", data.CurrentRowVersion.TheValue);
 command.Parameters.AddWithValue("@TextColumn", data.TextValue);
 command.Parameters.AddWithValue("@ValueColumn", data.NumberValue.HasValue 
? (object)data.NumberValue.Value : System.DBNull.Value);
 rowsAffected = command.ExecuteNonQuery();
 }
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnfalse;
 }
 returntrue;
}
'''<summary>''' Saves a single data item'''</summary>'''<paramname="data">Data to save</param>'''<returns>True if succesful</returns>PublicOverridesFunction SaveItem(data As Data.TheData) AsBooleanDim rowsAffected AsIntegerTryIf (Me.transaction IsNothing) ThenMe.transaction = Me.connection.BeginTransaction()
 EndIfUsing command As SqlCommand = New SqlCommand()
 command.Connection = Me.connection
 command.Transaction = Me.transaction
 command.CommandType = System.Data.CommandType.StoredProcedure
 command.CommandText = "procConcurrency2" command.Parameters.AddWithValue("@Id", data.Id)
 command.Parameters.AddWithValue("@CurrentVersion", data.CurrentRowVersion.TheValue)
 command.Parameters.AddWithValue("@TextColumn", data.TextValue)
 command.Parameters.AddWithValue("@ValueColumn", If(data.NumberValue.HasValue, _
 data.NumberValue.Value, System.DBNull.Value))
 rowsAffected = command.ExecuteNonQuery()
 EndUsingCatch exception As System.Exception
 System.Windows.MessageBox.Show(exception.Message, _
 exception.Source, _
 System.Windows.MessageBoxButton.OK, _
 System.Windows.MessageBoxImage.Error)
 ReturnFalseEndTryReturnTrueEndFunction

最大的区别是你不再在客户端调查更新的行的数量。 如果过程更新了错误的行数,则会引发错误,客户端会收到一个可以显示的异常。

强制使用触发器检查rowversion检查。

现在,你知道如何检查 rowversion,但是如何确保检查始终是正确的。 一个复杂的系统可以能有很多数据库操作,因这里如何保证每个数据库中的每一个。 答案是,你不能对所有代码进行这样的保证,但是可以强制在数据库中进行检查。

这个想法是在一个表中有两行版本列,一个保存rowversion当前值,另一个在更新完成时来自客户端。

第一个问题是单个表不能有多个类型为rowversion的列。 如果试图创建表 Having 两个rowversion列,则会收到如下错误

Msg 2738, Level 16, State 2, Line 1A table can only have one timestamp column. 
Because table 'table name' already has one, the column 'column name' cannot be added.

即使表可以包含两个rowversion列,因为它们都会自动更新。

因此,你实际需要的是二进制列。 由于列可以包含 null 值,因此需要将它的定义为 varbinary(8)。 表脚本可以如下所示

CREATETABLE Concurrency3 (
 Id intidentity (1,1) NOTNULL,
 CurrentVersion rowversion NOTNULL, PreviousVersion varbinary(8) NULL, TextColumn varchar(100) NULL,
 ValueColumn decimalNULL);

现在执行rowversion检查,无论客户端做什么,我们都需要一个触发器,它在更新时被触发。 触发器看起来像这样

-------------------------------------------- Trigger to enforce rowversion check------------------------------------------CREATETRIGGER Concurrency3_RowversionCheckON Concurrency3FORUPDATEASBEGINDECLARE@ErrorCountint;
 SELECT@ErrorCount = COUNT(*)
 FROM inserted i
 WHERENOTEXISTS (SELECT1 
 FROM deleted d
 WHERE d.Id = i.Id
 AND d.CurrentVersion = i.PreviousVersion);
 IF@ErrorCount> 0BEGINROLLBACK;
 RAISERROR('The given row versions do not match old row versions.', 16, 1);
 END;END;

为了理解触发器,你需要知道旧的更新行存储在虚拟表 deleted 表中,而新行的新版本存储在 inserted 虚拟表中。 因此,当更新发生时,你可以使用来自这些虚拟表的数据来调查更新的旧值和新值。

当更新发生时触发器期望客户端代码为列 PreviousVersion 提供了rowversion的值。 触发器调查 inserted ( 新建) 行中的值是否在( 旧的) 行中的CurrentVersion 列的值上进行调查。 如果在 deleted 表中没有找到相应的行,则会引发错误。

检查与 WHERE 子句中以前使用的检查有点不同。 原因是触发器每次执行只触发一次,所以如果你同时更新多行,则需要调查所有更新行。

现在,如果你看一下客户端的代码

///<summary>/// Saves a single data item///</summary>///<paramname="data">Data to save</param>///<returns>True if succesful</returns>overridepublicbool SaveItem(Data.IData data) {
 int rowsAffected;
 try {
 this.BeginTransaction();
 using (SqlCommand command = new SqlCommand()) {
 command.Connection = this.connection;
 command.Transaction = this.transaction;
 command.CommandText = @"UPDATE Concurrency3
SET TextColumn = @TextColumn,
 ValueColumn = @ValueColumn,
 PreviousVersion = @CurrentVersion
WHERE Id = @Id";
 command.Parameters.AddWithValue("@TextColumn", data.TextValue);
 command.Parameters.AddWithValue("@ValueColumn", data.NumberValue.HasValue 
? (object)data.NumberValue.Value : System.DBNull.Value);
 command.Parameters.AddWithValue("@CurrentVersion", data.CurrentRowVersion.TheValue);
 command.Parameters.AddWithValue("@Id", data.Id);
 rowsAffected = command.ExecuteNonQuery();
 if (rowsAffected!= 1) {
 thrownew Exception(string.Format("Wrong number of rows ({0}) affected", rowsAffected));
 }
 }
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnfalse;
 }
 returntrue;
}
'''<summary>''' Saves a single data item'''</summary>'''<paramname="data">Data to save</param>'''<returns>True if succesful</returns>PublicOverridesFunction SaveItem(data As Data.TheData) AsBooleanDim rowsAffected AsIntegerTryIf (Me.transaction IsNothing) ThenMe.transaction = Me.connection.BeginTransaction()
 EndIfUsing command As SqlCommand = New SqlCommand()
 command.Connection = Me.connection
 command.Transaction = Me.transaction
 command.CommandText = "UPDATE Concurrency3
SET TextColumn = @TextColumn,
 ValueColumn = @ValueColumn,
 PreviousVersion = @CurrentVersion
WHERE Id = @Id" command.Parameters.AddWithValue("@TextColumn", data.TextValue)
 command.Parameters.AddWithValue("@ValueColumn", If(data.NumberValue.HasValue, _
 data.NumberValue.Value, System.DBNull.Value))
 command.Parameters.AddWithValue("@CurrentVersion", data.CurrentRowVersion.TheValue)
 command.Parameters.AddWithValue("@Id", data.Id)
 rowsAffected = command.ExecuteNonQuery()
 If (rowsAffected <> 1) ThenThrowNew Exception(String.Format("Wrong number of rows ({0}) affected", rowsAffected))
 EndIfEndUsingCatch exception As System.Exception
 System.Windows.MessageBox.Show(exception.Message, _
 exception.Source, _
 System.Windows.MessageBoxButton.OK, _
 System.Windows.MessageBoxImage.Error)
 ReturnFalseEndTryReturnTrueEndFunction

你可以看到 WHERE 子句中没有附加条件,只是主键的条件。 相反,PreviousVersion 列在 UPDATE 语句中设置为具有当前在客户端上的rowversion的值。

另外,请注意,仅当主键无效时检查更新了多少行失败。

现在,如果再次重复列出的测试,你将收到如下错误

使用 SQL Server Management Studio,你还可以尝试更新多行而不提供正确的旧 rowversion,例如

-- Try to update several rows at a timeUPDATE Concurrency3 SET ValueColumn = ValueColumn + 1;

这导致了一个错误

-- Msg 50000, Level 16, State 1, Procedure Concurrency3_RowversionCheck, Line 16 [Batch Start Line 91]-- The given row versions do not match old row versions.-- Msg 3609, Level 16, State 1, Line 93-- The transaction ended in the trigger. The batch has been aborted.

与 Entity Framework ( 仅 C# ) 对应。

现在我们只使用纯SQL讨论了这个问题,那么 Entity Framework 又如何? 如何在使用EF时强制执行相同的检查?

这个想法自然是一样的,将条件嵌入到执行的SQL语句中。 为了使示例更直观,可以将 Id 列定义为主键,从而可以正确地选择表的实际键。

CREATETABLE Concurrency5 (
 Id intidentity (1,1) 
 PRIMARYKEYNOTNULL,
 CurrentVersion rowversion NOTNULL,
 TextColumn varchar(100) NULL,
 ValueColumn decimalNULL);

下一步是从数据库中生成模型。 由于它主要是通过单击向导中的下一步按钮来完成的,所以我不会详细介绍。 IMPORTANT的连接字符串在 app.config 文件中,因此修改它以便在你的环境中包含正确的实例和数据库名称。!

创建模型之后,让我们看一下获取和保存的方法

///<summary>/// Fetch the data from the database and return as a collection///</summary>///<returns>Collection of data items</returns>overridepublic ObservableCollection<Data.IData> FetchData() {
 try {
 if (this.CurrentDataCollection == null) {
 this.CurrentDataCollection = new ObservableCollection<Data.IData>();
 foreach (Concurrency5 item inthis.CurrentContext.Concurrency5) {
 this.CurrentDataCollection.Add(item);
 }
 } else {
 foreach (Concurrency5 item inthis.CurrentDataCollection) {
 this.CurrentContext.Entry(item).Reload();
 }
 this.CurrentDataCollection = new ObservableCollection<Data.IData>(this.CurrentDataCollection);
 }
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnthis.CurrentDataCollection;
 }
 returnthis.CurrentDataCollection;
}

上面的代码从 DbContext 获取项的集合。 代码Fragment很简单,只需用 Concurrency5 表中的项填充集合。 代码中的特性很少。 首先,如果集合已经存在,我只需要从数据库中单独重新加载项。 另外,返回的集合始终是一个新集合。 由于我没有实现 INotifyPropertyChanged 接口,这只是一个快速解决办法,迫使网格对可能发生的更改做出React。

如上例所示,IMPORTANT 同样在上下文和事务的生存期中也不必要延长。! 原因是这种方式更容易测试不同的场景,但是保留上下文不应该在实际情况下完成。

那么保存看起来像什么? 以最简单的形式

///<summary>/// Saves a single data item///</summary>///<paramname="data">Data to save</param>///<returns>True if succesful</returns>overridepublicbool SaveItem(Data.IData data) {
 int rowsAffected;
 try {
 this.BeginTransaction();
 rowsAffected = this.CurrentContext.SaveChanges();
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnfalse;
 }
 returntrue;
}

如你所见,没有执行rowversion检查的代码,没有修改 WHERE 子句和存储受影响行的数量。 如果你要跟踪执行的SQL语句,你会注意到 WHERE 子句只包含主键的条件,它的他内容没有。

那么如何使rowversion检查发生? 关键是改变你的效能模型。 如果打开模型并单击 CurrentVersion 列,则可以看到 ConcurrencyMode 属性更改为 Fixed。 这是通知有效并发检查要实施的方法。

那么代码中发生了什么,如何检查是否已经更新了正确的行数? 答案是你不需要检查它。 如果更新未更新行,则EF将引发一个 DbUpdateConcurrencyException。 所以完整的代码看起来像这样

///<summary>/// Saves a single data item///</summary>///<paramname="data">Data to save</param>///<returns>True if succesful</returns>overridepublicbool SaveItem(Data.IData data) {
 int rowsAffected;
 try {
 this.BeginTransaction();
 rowsAffected = this.CurrentContext.SaveChanges();
 } catch (System.Data.Entity.Infrastructure.DbUpdateConcurrencyException concurrencyException) {
 System.Windows.MessageBox.Show("Row versions do not match (" + concurrencyException.Message + ")",
 concurrencyException.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnfalse;
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnfalse;
 }
 returntrue;
}


如果你重复前面描述的相同测试,你应该会看到类似于"存储更新。插入或者 delete 语句影响unwxpected行数量( 0 )的消息。 实体可能已经被修改或者删除,因为实体已经加载。 "

更新rowversion值( 仅 C# )

尚未回答的明显问题是,使用纯SQL时,rowversion的值应如何为 refetched? 当然可以重新查询值,但是这将是数据库的额外旅行。 为了避免不必要的网络流量,可以在一次运行中包含两个语句。 下面是一个例子

///<summary>/// Saves a single data item///</summary>///<paramname="data">Data to save</param>///<returns>True if succesful</returns>overridepublicbool SaveItem(Data.IData data) {
 System.Collections.Generic.List<Data.IData> oldItems;
 try {
 this.BeginTransaction();
 using (SqlCommand command = new SqlCommand()) {
 command.Connection = this.Connection;
 command.Transaction = this.Transaction;
 command.CommandText = @"UPDATE Concurrency2
SET TextColumn = @TextColumn,
 ValueColumn = @ValueColumn
WHERE Id = @Id
AND CurrentVersion = @CurrentVersion;
SELECT @@ROWCOUNT AS AffectedRows, 
 CurrentVersion AS CurrentVersion
FROM Concurrency2
WHERE Id = @Id";
 command.Parameters.AddWithValue("@TextColumn", data.TextValue);
 command.Parameters.AddWithValue("@ValueColumn", data.NumberValue.HasValue 
? (object)data.NumberValue.Value 
 : System.DBNull.Value);
 command.Parameters.AddWithValue("@Id", data.Id);
 command.Parameters.AddWithValue("@CurrentVersion", data.CurrentRowVersion.TheValue);
 using (SqlDataReader reader = command.ExecuteReader()) {
 if (!reader.Read()) {
 thrownew System.ApplicationException("Row has been deleted");
 }
 if ((int)reader["AffectedRows"]!= 1) {
 thrownew System.Exception("Row versions do not match.");
 }
 ((Data.TheData)data).CurrentRowVersion = new Data.Rowversion();
 reader.GetBytes(1, 0, data.CurrentRowVersion.TheValue, 0, 8);
 }
 }
 oldItems = new System.Collections.Generic.List<Data.IData>();
 oldItems.AddRange(this.CurrentDataCollection);
 this.CurrentDataCollection.Clear();
 oldItems.ForEach(x =>this.CurrentDataCollection.Add(x));
 } catch (System.Exception exception) {
 System.Windows.MessageBox.Show(exception.Message, 
 exception.Source, 
 System.Windows.MessageBoxButton.OK, 
 System.Windows.MessageBoxImage.Error);
 returnfalse;
 }
 returntrue;
}

当你看到SQL文本包含两个单独的sql语句时,一个用于更新行,另一个用于从行中获取新值。 SELECT 部件获取:

  • 更新的行数( @@ROWCOUNT )
  • 来自 CurrentVersion 列的新值

对于提取的值,客户端代码检查实际更新的行数量是否正确,如果是 true,则使用数据库中的新值存储最新的rowversion 值。

关键点

本文中的主要内容是:

  • 即使使用悲观锁定,也经常需要行版本检查
  • 如何利用 rowversion 实现这个检查
  • 如何在过程中检查 rowversion
  • 如何强制客户端程序使用触发器提供正确的rowversion
  • 如何使用 Entity Framework 完成检查

通过使用这些原则,你可以使程序更加健壮,并确保用户总是知道自己的更新。

另外,请注意,本文仅涉及 UPDATE 场景。 在许多情况下,对 DELETE 执行相等的测试也是有意义的。

另外一个很好的发现是,你可以轻松地用 Entity Framework 跟踪sql语句。 在 C# 代码中,查看 ConcurrencyContext_CustomConnectionString 类将附加构造函数添加到上下文中,但也向 Log 属性添加一个方法,以便将日志信息写入 Debug

///<summary>/// This partial definition extends the EF generated context class///</summary>publicpartialclass ConcurrencyContext : System.Data.Entity.DbContext {
 ///<summary>/// Constructor for using custom connection string///</summary>///<paramname="connectionString">Connections string to use</param>public ConcurrencyContext(string connectionString) : base(connectionString) {
 this.Database.Log = WriteLog;
 }
 ///<summary>/// Write the message into debug output ///</summary>///<paramname="message">Message to write</param>publicvoid WriteLog(string message) {
 System.Diagnostics.Debug.WriteLine(message);
 }
}

引用

历史记录

  • 23rd 2017年03月: 已经创建。
  • 第二个 2017年04月: Oracle替代方案。
  • 10th 2017年04月,C# 版本增加
    • 添加了登录屏幕
    • 添加了一个使用EF6的示例
    • 添加了一个示例,如何在更新数据库的同一调用中获取新的rowversion
  • 第一个 2017年05月: 从固定连接字符串中删除剩余的余数,增强术语准确性。



Copyright © 2011 HelpLib All rights reserved.    知识分享协议 京ICP备05059198号-3  |  如果智培  |  酷兔英语