乐观并发:IsConcurrencyToken和RowVersion

时间:2015-07-09 23:25:28

标签: c# sql-server entity-framework concurrency ef-code-first

我正在创建我将在我的应用程序中使用的默认并发策略。

我决定采取乐观的策略。

我的所有实体都映射为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();

3 个答案:

答案 0 :(得分:19)

在EF6和EF-core中,使用Sql Server时,必须使用此映射:

modelBuilder.Entity<Product>() 
.Property(t => t.RowVersion) 
.IsRowVersion(); // Not: IsConcurrencyToken

IsConcurrencyToken 确实将属性配置为并发令牌,但(将其用于byte[]属性时)

  • 数据类型为varbinary(max)
  • 如果您不初始化,则其值始终为null
  • 更新记录时,其值不会自动递增。
另一方面,

IsRowVersion

  • 的数据类型为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;