乐观并发:IsConcurrencyToken 和 RowVersion
Optimistic concurrency: IsConcurrencyToken and RowVersion
我正在创建将在我的应用程序中使用的默认并发策略。
我决定采用乐观策略。
我的所有实体都映射为 Table per Type (TPT)
(使用继承)。我很快了解到,在 Entity Framework:
上使用带有继承的 RowVersion 类型的列时会出现问题
Product
Id INT IDENTITY PRIMARY KEY
RowVersion ROWVERSION
Car (inherits Product records)
Color TYNIINT NOT NULL,
AnotherProperty....
如果我更新 Car
table 的记录,Product
table 中的 RowVersion 列将不会更新。
我计划在 Product
中使用 datetime2 (7)
类型的列,如果继承此 table 的 table 的任何记录被修改,则手动更新它。
我想我正在重新发明轮子。
在Entity Framework中使用Table per Type (TPT)
时,还有其他方法可以使用ROWVERSION
的乐观并发策略吗?
编辑
我的映射:
class Product
{
int Id { get; set; }
string Name { get; set; }
byte[] RowVersion { get; set; }
}
class Car : Product
{
int Color { get; set; }
}
CodeFirst 约定。
只有 Product
实体上的 RowVersion 属性 具有自定义定义:
modelBuilder.Entity<Product>()
.Property(t => t.RowVersion)
.IsConcurrencyToken();
在 EF6 和 EF-core 中,当使用 SQL 服务器时,您必须使用此映射:
modelBuilder.Entity<Product>()
.Property(t => t.RowVersion)
.IsRowVersion(); // Not: IsConcurrencyToken
IsConcurrencyToken 确实将 属性 配置为并发令牌,但是(将其用于 byte[]
属性 时)
- 数据类型为
varbinary(max)
- 如果你不初始化它,它的值总是
null
- 更新记录时其值不会自动递增。
IsRowVersion另一方面,
- 具有数据类型
rowversion
(在 SQL 服务器中,或在早期版本中 timestamp
),因此
- 它的值永远不会为空,并且
- 它的值总是在更新记录时自动递增。
- 它会自动将 属性 配置为开放式并发令牌。
现在,当您更新 Car
时,您会看到两个更新语句:
DECLARE @p int
UPDATE [dbo].[Product]
SET @p = 0
WHERE (([Id] = @0) AND ([Rowversion] = @1))
SELECT [Rowversion]
FROM [dbo].[Product]
WHERE @@ROWCOUNT > 0 AND [Id] = @0
UPDATE [dbo].[Car]
SET ...
第一个语句不更新任何内容,但它增加了行版本,如果行版本在其间发生更改,它将抛出并发异常。
[System.ComponentModel.DataAnnotations.Schema.Timestamp]
属性是相当于 IsRowVersion()
:
的数据注释
[Timestamp]
public byte[] RowVersion { get; set; }
请注意 official documentation 不正确。它说 IsConcurrencyToken
是 [Timestamp]
属性的流利等价物。但是,IsRowVersion
是等效的。
经过一番调查后,我能够在 Entity Framework 6.
中名为 RowVersion 的 byte[8] 列上使用 IsConcurrencyToken
因为我们想在 DB2 中使用相同的数据类型(它在数据库本身中没有行版本),所以我们不能使用选项 IsRowVersion()!
我进一步调查了如何使用 IsConcurrencyToken。
我做了以下工作以获得似乎有效的解决方案:
我的模特:
public interface IConcurrencyEnabled
{
byte[] RowVersion { get; set; }
}
public class Product : AuditableEntity<Guid>,IProduct,IConcurrencyEnabled
{
public string Name
{
get; set;
}
public string Description
{
get; set;
}
private byte[] _rowVersion = new byte[8];
public byte[] RowVersion
{
get
{
return _rowVersion;
}
set
{
System.Array.Copy(value, _rowVersion, 8);
}
}
}
IConcurrencyEnabled 用于标识具有需要特殊处理的行版本的实体。
我使用 fluent API 配置模型构建器:
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
public ProductConfiguration()
{
Property(e => e.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
Property(e => e.RowVersion).IsFixedLength().HasMaxLength(8).IsConcurrencyToken();
}
}
最后,我在派生的 DBContext class 中添加了一个方法,以便在调用 base.SaveChanges 之前更新字段:
public void OnBeforeSaveChanges(DbContext dbContext)
{
foreach (var dbEntityEntry in dbContext.ChangeTracker.Entries().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified))
{
IConcurrencyEnabled entity = dbEntityEntry.Entity as IConcurrencyEnabled;
if (entity != null)
{
if (dbEntityEntry.State == EntityState.Added)
{
var rowversion = dbEntityEntry.Property("RowVersion");
rowversion.CurrentValue = BitConverter.GetBytes((Int64)1);
}
else if (dbEntityEntry.State == EntityState.Modified)
{
var valueBefore = new byte[8];
System.Array.Copy(dbEntityEntry.OriginalValues.GetValue<byte[]>("RowVersion"), valueBefore, 8);
var value = BitConverter.ToInt64(entity.RowVersion, 0);
if (value == Int64.MaxValue)
value = 1;
else value++;
var rowversion = dbEntityEntry.Property("RowVersion");
rowversion.CurrentValue = BitConverter.GetBytes((Int64)value);
rowversion.OriginalValue = valueBefore;//This is the magic line!!
}
}
}
}
大多数人遇到的问题是,在设置实体的值后,我们总是得到一个 UpdateDBConcurrencyException,因为 OriginalValue 已经改变了......即使没有!
原因是对于 byte[],如果单独设置 CurrentValue,original 和 currentValue 都会改变(??奇怪和意外的行为)。
所以我在更新行版本之前再次将 OriginalValue 设置为原始值...
我还复制数组以避免引用相同的字节数组!
注意:这里我使用增量的方式改变rowversion,你可以随意使用自己的策略来填充这个值。 (随机或基于时间)
问题不在于您的设置方式。发生的事情是,一旦您将 RowVersion
条目从上下文中拉出,它的 OriginalValue
就会设置为新值。
var carInstance = dbContext.Cars.First();
carInstance.RowVersion = carDTO.RowVerison;
carInstance.Color = carDTO.Color ;
var entry = dbContext.Entry(carInstance); //Can also come from ChangeTrack in override of SaveChanges (to do it automatically)
entry.Property(e => e.RowVersion)
.OriginalValue = entry.Entity.RowVersion;
我正在创建将在我的应用程序中使用的默认并发策略。
我决定采用乐观策略。
我的所有实体都映射为 Table per Type (TPT)
(使用继承)。我很快了解到,在 Entity Framework:
Product
Id INT IDENTITY PRIMARY KEY
RowVersion ROWVERSION
Car (inherits Product records)
Color TYNIINT NOT NULL,
AnotherProperty....
如果我更新 Car
table 的记录,Product
table 中的 RowVersion 列将不会更新。
我计划在 Product
中使用 datetime2 (7)
类型的列,如果继承此 table 的 table 的任何记录被修改,则手动更新它。
我想我正在重新发明轮子。
在Entity Framework中使用Table per Type (TPT)
时,还有其他方法可以使用ROWVERSION
的乐观并发策略吗?
编辑
我的映射:
class Product
{
int Id { get; set; }
string Name { get; set; }
byte[] RowVersion { get; set; }
}
class Car : Product
{
int Color { get; set; }
}
CodeFirst 约定。
只有 Product
实体上的 RowVersion 属性 具有自定义定义:
modelBuilder.Entity<Product>()
.Property(t => t.RowVersion)
.IsConcurrencyToken();
在 EF6 和 EF-core 中,当使用 SQL 服务器时,您必须使用此映射:
modelBuilder.Entity<Product>()
.Property(t => t.RowVersion)
.IsRowVersion(); // Not: IsConcurrencyToken
IsConcurrencyToken 确实将 属性 配置为并发令牌,但是(将其用于 byte[]
属性 时)
- 数据类型为
varbinary(max)
- 如果你不初始化它,它的值总是
null
- 更新记录时其值不会自动递增。
IsRowVersion另一方面,
- 具有数据类型
rowversion
(在 SQL 服务器中,或在早期版本中timestamp
),因此 - 它的值永远不会为空,并且
- 它的值总是在更新记录时自动递增。
- 它会自动将 属性 配置为开放式并发令牌。
现在,当您更新 Car
时,您会看到两个更新语句:
DECLARE @p int
UPDATE [dbo].[Product]
SET @p = 0
WHERE (([Id] = @0) AND ([Rowversion] = @1))
SELECT [Rowversion]
FROM [dbo].[Product]
WHERE @@ROWCOUNT > 0 AND [Id] = @0
UPDATE [dbo].[Car]
SET ...
第一个语句不更新任何内容,但它增加了行版本,如果行版本在其间发生更改,它将抛出并发异常。
[System.ComponentModel.DataAnnotations.Schema.Timestamp]
属性是相当于 IsRowVersion()
:
[Timestamp]
public byte[] RowVersion { get; set; }
请注意 official documentation 不正确。它说 IsConcurrencyToken
是 [Timestamp]
属性的流利等价物。但是,IsRowVersion
是等效的。
经过一番调查后,我能够在 Entity Framework 6.
中名为 RowVersion 的 byte[8] 列上使用 IsConcurrencyToken因为我们想在 DB2 中使用相同的数据类型(它在数据库本身中没有行版本),所以我们不能使用选项 IsRowVersion()!
我进一步调查了如何使用 IsConcurrencyToken。
我做了以下工作以获得似乎有效的解决方案:
我的模特:
public interface IConcurrencyEnabled
{
byte[] RowVersion { get; set; }
}
public class Product : AuditableEntity<Guid>,IProduct,IConcurrencyEnabled
{
public string Name
{
get; set;
}
public string Description
{
get; set;
}
private byte[] _rowVersion = new byte[8];
public byte[] RowVersion
{
get
{
return _rowVersion;
}
set
{
System.Array.Copy(value, _rowVersion, 8);
}
}
}
IConcurrencyEnabled 用于标识具有需要特殊处理的行版本的实体。
我使用 fluent API 配置模型构建器:
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
public ProductConfiguration()
{
Property(e => e.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
Property(e => e.RowVersion).IsFixedLength().HasMaxLength(8).IsConcurrencyToken();
}
}
最后,我在派生的 DBContext class 中添加了一个方法,以便在调用 base.SaveChanges 之前更新字段:
public void OnBeforeSaveChanges(DbContext dbContext)
{
foreach (var dbEntityEntry in dbContext.ChangeTracker.Entries().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified))
{
IConcurrencyEnabled entity = dbEntityEntry.Entity as IConcurrencyEnabled;
if (entity != null)
{
if (dbEntityEntry.State == EntityState.Added)
{
var rowversion = dbEntityEntry.Property("RowVersion");
rowversion.CurrentValue = BitConverter.GetBytes((Int64)1);
}
else if (dbEntityEntry.State == EntityState.Modified)
{
var valueBefore = new byte[8];
System.Array.Copy(dbEntityEntry.OriginalValues.GetValue<byte[]>("RowVersion"), valueBefore, 8);
var value = BitConverter.ToInt64(entity.RowVersion, 0);
if (value == Int64.MaxValue)
value = 1;
else value++;
var rowversion = dbEntityEntry.Property("RowVersion");
rowversion.CurrentValue = BitConverter.GetBytes((Int64)value);
rowversion.OriginalValue = valueBefore;//This is the magic line!!
}
}
}
}
大多数人遇到的问题是,在设置实体的值后,我们总是得到一个 UpdateDBConcurrencyException,因为 OriginalValue 已经改变了......即使没有!
原因是对于 byte[],如果单独设置 CurrentValue,original 和 currentValue 都会改变(??奇怪和意外的行为)。
所以我在更新行版本之前再次将 OriginalValue 设置为原始值... 我还复制数组以避免引用相同的字节数组!
注意:这里我使用增量的方式改变rowversion,你可以随意使用自己的策略来填充这个值。 (随机或基于时间)
问题不在于您的设置方式。发生的事情是,一旦您将 RowVersion
条目从上下文中拉出,它的 OriginalValue
就会设置为新值。
var carInstance = dbContext.Cars.First();
carInstance.RowVersion = carDTO.RowVerison;
carInstance.Color = carDTO.Color ;
var entry = dbContext.Entry(carInstance); //Can also come from ChangeTrack in override of SaveChanges (to do it automatically)
entry.Property(e => e.RowVersion)
.OriginalValue = entry.Entity.RowVersion;