使用EF Core和MySQL实现行版本的更好方法是什么?

7
如果我在我的模型中使用以下字段:
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[Timestamp]
public DateTime RowVersion { get; set; }

然后将该列定义为

`RowVersion` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

我从EF中得到了适当的乐观并发行为。尽管如此,我对使用时间戳并不感到满意,因为它似乎只有秒级分辨率。虽然在1秒内有2个客户端尝试更新相同的记录的机会不大,但这确实可能发生,不是吗?

因此,考虑到这一点,我更喜欢一个简单的整数,每次更新时原子地增加1。这样就不可能错过冲突。我将定义更改为:

[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[Timestamp]
public long RowVersion { get; set; }

问题是,MySQL不会自动增加这个值。因此,我创建了一个触发器:
CREATE TRIGGER update_row_version BEFORE UPDATE on client 
FOR EACH ROW
SET NEW.RowVersion = OLD.RowVersion + 1;

现在这一切都能够正常工作了。EF会在必要的时候抛出DbUpdateConcurrencyException异常,且不会因为时间窗口而错过更新。但是,它使用了触发器,我一直在阅读关于它们性能如何糟糕的信息。

那么有更好的方法吗?也许可以覆盖DbContext的SaveChanges()方法,在客户端执行RowVersion增量操作,因此只在数据库上进行单个更新(我假设触发器每次实际上会进行两次更新)?


[Timestamp]public byte[] RowVersion 这个代码能用吗?通常是用于 MSSQL,但不知道 MySQL Provider 是否也能处理它。与 MSSQL 不同,MySQL 没有行版本数据类型。 - Tseng
关于覆盖 SaveChanges... 不行。它只有在保存前想要更改值时才有用,但必须保持不变,并且仅在保存查询期间更改。RowVersion 将在更新查询中用于 where 条件:WHERE Id=? AND RowVersion=? - Tseng
CURRENT_TIMESTAMP 的精度为微秒级(6位数字)。我不会过于担心同步更新。 - Gert Arnold
@Tseng 我也尝试过使用 byte[] 类型,但问题在于无论如何,使用 [Timestamp] 属性时,EF 都期望数据库修改值。如果我要摆脱触发器,我需要在客户端进行操作。 - Jeff
你可以尝试重写 SaveChanges 方法,在所有修改的记录上递增该值。我不确定这是否有效,因为我不确定 EF 是否会使用当前记录的值还是先前的值(如果未明确禁用,则 EF Core 会跟踪更改)。 - Tseng
显示剩余4条评论
1个回答

10

好的,我想出了一种策略,似乎不需要任何触发器就能很好地工作。

我添加了一个简单的界面:

interface ISavingChanges
{
    void OnSavingChanges();
}

现在模型看起来像这样:

public class Client : ISavingChanges
{
    // other fields omitted for clarity...


    [ConcurrencyCheck]
    public long RowVersion { get; set; }

    public void OnSavingChanges()
    {
        RowVersion++;
    }
}

然后我像这样覆盖了SaveChanges:

    public override int SaveChanges()
    {
        foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified))
        {
            var saveEntity = entity.Entity as ISavingChanges;
            saveEntity.OnSavingChanges();
        }

        return base.SaveChanges();
    }

这一切都按预期进行。ConcurrencyCheck属性是让EF将RowVersion字段包含在UPDATE SQL的SET和WHERE子句中的关键。


2
当我在ASP.NET Core应用程序中使用Pomelo.EntityFrameworkCore.MySql驱动程序时,我不得不使用这个示例。虽然我更喜欢使用Fluent API来进行此类配置。与MS文档此处中的内容相比,我不得不进行一些修改。 modelBuilder.Entity() .Property(p => p.RowVersion) .HasDefaultValue(0) .IsConcurrencyToken(); - David Guerin
在保存更改之前,您调用了什么来跟踪实体的“修改”状态?我似乎无法让它工作。我的版本从未真正增加。 - cvb
可能 SavingChanges 事件更适合这个操作,因为 SaveChanges() 和 SaveChangesAsync() 都有可能被调用。 - Alex Buchatski
可能SavingChanges事件是更好的位置,因为SaveChanges()和SaveChangesAsync()都可能被调用。 - undefined

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