乐观并发:IsConcurrencyToken和RowVersion

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了乐观并发:IsConcurrencyToken和RowVersion相关的知识,希望对你有一定的参考价值。

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

我决定采取乐观的策略。

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

Code First惯例。

只有Product实体上的RowVersion属性具有自定义定义:

modelBuilder.Entity<Product>() 
    .Property(t => t.RowVersion) 
    .IsConcurrencyToken();
答案

在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()等效的数据注释:

1617739030
public byte[] RowVersion { get; set; }
另一答案

经过一些调查后,我能够在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,您可以自由地使用自己的策略来填充此值。 (随机或基于时间)

另一答案

问题不在于您的设置方式。发生的事情是,只要将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;

以上是关于乐观并发:IsConcurrencyToken和RowVersion的主要内容,如果未能解决你的问题,请参考以下文章

悲观锁 乐观锁

乐观锁vs悲观锁

悲观锁和乐观锁,啥情况

乐观锁和悲观锁

多表复杂实体的乐观并发

Elasticsearch的乐观并发控制和分片管理(更新中)