乐观并发:IsConcurrencyToken和RowVersion

23

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

我选择了一种乐观的策略。

我所有的实体都被映射为每类型表(TPT)(使用继承)。很快我就发现,在Entity Framework上使用RowVersion类型的列时,在继承方面会出现问题:

Product

Id INT IDENTITY PRIMARY KEY
RowVersion ROWVERSION

Car (inherits Product records)

Color TYNIINT NOT NULL,
AnotherProperty....   

如果我更新“Car”表的记录,则不会更新“Product”表的RowVersion列。
我打算在“Product”中使用datetime2 (7)类型的列,如果继承此表的任何表的记录被修改,则手动更新它。
我认为我正在重新发明轮子。
当使用实体框架中的“表格对类(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();

产品表不会被更新。实际上,它会进行虚拟更新以增加行版本(EF 6.1.3)。 - Gert Arnold
好的,也许你应该展示一下你的映射,看看我们是否在同一个页面上。 - Gert Arnold
我只能在回答中发布代码。如果没有看到你的映射,我不认为我能回答你的问题。我只是使用了一个食谱 TPT 映射。 - Gert Arnold
映射已添加。@GertArnold - Vinicius Gonçalves
@kirsteng,这个简短的描述让我无法做出太多事情。你最好在一个新问题中更完整地解释一下。 - Gert Arnold
显示剩余4条评论
3个回答

41

在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 ...

第一个语句并不更新任何内容,但它会增加行版本,并在行版本在此期间更改时引发并发异常。

[System.ComponentModel.DataAnnotations.Schema.Timestamp] 属性是数据注释中 IsRowVersion() 的等效项:

[Timestamp]
public byte[] RowVersion { get; set; }

请注意,官方文档是不正确的。它说IsConcurrencyToken[Timestamp]属性的流畅等效项。然而,IsRowVersion才是等效项。

你使用的是哪个版本的SQL Server?根据文档,对于byte[]属性使用IsConcurrencyToken()应该映射到在SQL Server 2005中添加的rowversion数据类型。 - Søren Boisen
@SørenBoisen 我记得这是SQL2012。我刚刚测试了一下代码优先,然后报告了我得到的结果。如果文档说其他的事情,那么要么是错误的,要么是它预设了我没有采取的其他步骤。你指的是哪个文档? - Gert Arnold
也许不完全是文档,但仍然是官方网站上的教程 :-) 在这里:http://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application - Søren Boisen
顺便说一下,我可以确认你的发现 - 需要使用IsRowVersion来获得正确的行为。 - Søren Boisen
5
文件确实有些不准确。它展示了[Timestamp]属性的使用,然后说明IsConcurrencyToken是等效的流畅方法。但这并不正确。IsRowVersion才是等效的。 - Gert Arnold
显示剩余2条评论

7

经过一番调查,我成功地在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,则OriginalValue和CurrentValue都会更改(奇怪和意外的行为)。
所以我再次将OriginalValue设置为更新rowversion之前的原始值... 此外,我复制数组以避免引用相同的字节数组!
注意:这里我使用增量方法来更改rowversion,您可以自由选择自己的策略来填充此值(随机或基于时间)。

1
问题不在于你的设置。问题在于当你从上下文中取出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;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接