我正在创建我将在我的应用程序中使用的默认并发策略。
我决定采取乐观的策略。
我的所有实体都映射为Table per Type (TPT)
(使用继承)。我很快就了解到在实体框架上使用RowVersion类型的列并继承时存在问题:
Product
Id INT IDENTITY PRIMARY KEY
RowVersion ROWVERSION
Car (inherits Product records)
Color TYNIINT NOT NULL,
AnotherProperty....
如果我更新Car
表的记录,Product
表中的RowVersion列将不会更新。
我计划在datetime2 (7)
中使用Product
类型的列,并且如果修改了继承此表的表的任何记录,则手动更新它。
我想我正在重新发明轮子。
在实体框架中使用ROWVERSION
时,是否有另一种方法可以将乐观并发策略与Table per Type (TPT)
一起使用?
修改
我的映射:
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();
答案 0 :(得分:19)
在EF6和EF-core中,使用Sql Server时,必须使用此映射:
modelBuilder.Entity<Product>()
.Property(t => t.RowVersion)
.IsRowVersion(); // Not: IsConcurrencyToken
IsConcurrencyToken 确实将属性配置为并发令牌,但(将其用于byte[]
属性时)
varbinary(max)
null
rowversion
(在Sql Server中,或早期版本为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 ...
第一个语句不会更新任何内容,但会增加rowversion,如果在中间更改了rowversion,它将抛出并发异常。
[System.ComponentModel.DataAnnotations.Schema.Timestamp]
属性是等同于IsRowVersion()
的数据注释:
[Timestamp]
public byte[] RowVersion { get; set; }
答案 1 :(得分:3)
经过一些调查后,我能够在Entity Framework 6中名为RowVersion的byte [8]列上使用IsConcurrencyToken。
因为我们想在DB2中使用相同的数据类型(数据库本身没有rowversion),所以我们不能使用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用于标识具有需要特殊处理的rowversion的实体。
我使用了流畅的API来配置模型构建器:
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
public ProductConfiguration()
{
Property(e => e.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
Property(e => e.RowVersion).IsFixedLength().HasMaxLength(8).IsConcurrencyToken();
}
}
最后,我在派生的DBContext类中添加了一个方法,以便在调用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都会发生变化(奇怪且意外的行为)。
所以我在更新rowversion之前再次将OriginalValue设置为原始值... 我也复制数组以避免引用相同的字节数组!
注意:这里我使用增量方法来更改rowversion,您可以自由使用自己的策略来填充此值。 (随机或基于时间的)
答案 2 :(得分:0)
问题不在于您的设置方式。发生的事情是,只要您将OriginalValue
条目的RowVersion
设置为新值,就会将其设置为新值。
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;