【问题标题】:DbUpdateConcurrencyException using Entity Framework 6 with MySqlDbUpdateConcurrencyException 使用 Entity Framework 6 和 MySql
【发布时间】:2014-08-29 23:27:06
【问题描述】:

我在使用 EF6 和 MySQL 进行并发检查时遇到问题。

我遇到的问题是当我尝试将数据保存到数据库时引发并发异常。如果您检查输出到控制台的 sql,它会尝试使用 where 子句中的旧值从数据库中查询并发字段。因为这个字段已经被数据库更新了。

环境:

  • Windows 7 64 位
  • Visual Studio 2013

已安装 Nuget 包:

  • EF 6.0.1
  • MySql.ConnectorNET.Data 6.8.3.2
  • MySql.ConnectorNET.Entity 6.8.3.2

演示数据库 SQL:

DROP DATABASE IF EXISTS `bugreport`;
CREATE DATABASE IF NOT EXISTS `bugreport`;
USE `bugreport`;

DROP TABLE IF EXISTS `test`;
CREATE TABLE IF NOT EXISTS `test` (
  `TestId` int(10) NOT NULL AUTO_INCREMENT,
  `AStringField` varchar(50) DEFAULT NULL,
  `DateModified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`TestId`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;

INSERT INTO `test` (`TestId`, `AStringField`, `DateModified`) VALUES
    (1, 'Initial Value', '2014-07-11 09:15:52');

演示代码:

using System;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;

namespace BugReport
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new BugReportModel())
            {
                context.Database.Log = (s => Console.WriteLine(s));

                var firstTest = context.tests.First();
                firstTest.AStringField = "First Value";

                // Exception is thrown when changes are saved.
                context.SaveChanges();              

                Console.ReadLine();
            } 
        }
    }

    public class BugReportModel : DbContext
    {
        public BugReportModel()
            : base("name=Model1")
        {

        }

        public virtual DbSet<test> tests { get; set; }
    }


    [Table("test")]
    public class test
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int TestId { get; set; }

        [StringLength(50)]
        public string AStringField { get; set; }

        [ConcurrencyCheck()]
        [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
        [Column(TypeName = "timestamp")]
        public System.DateTime DateModified { get; set; }
    }
}

更新: 用 MySql 归档 bug。

【问题讨论】:

  • 代码优先?为什么读取行后并发字段会发生变化?如果 EF 中标记为 Concurrency 的字段在读取后和更新之前发生更改,则 EF 确实正确抛出 Concurrency 异常
  • 我阅读了该行,然后在下一行修改它。保存更改会引发异常。修改记录应该更新并发字段(即 rowversion)。我试图通过将 context.SaveChanges() 调用包装在 Try-Catch 块中以显示引发的异常来引起对它的注意。
  • 是的,在这种情况下它是 Code First。
  • 不能更改并发字段
  • 这就是我修改'AStringField'的原因。

标签: mysql entity-framework concurrency entity-framework-6


【解决方案1】:

您应该尝试使用 DB Timestamp / Rowversion 功能。 在 EF 中,您声明一个 ByteArray 并将其指定为并发检查字段。 DB 设定创造的价值。所有后续更新都可以检查值没有改变 DB 根据需要更新 rowversion。这种方法适用于 SQL 服务器。 它在 MYSql 上的行为应该相同。

    public  abstract class BaseObject  {
    [Key]
    [Required]
    public virtual int Id { set; get; }

    [ConcurrencyCheck()]
    public virtual byte[] RowVersion { get; set; }

    }

如果你愿意,也可以通过 fluent // 首要的关键 this.HasKey(t => t.Id);

        // Properties
        //Id is an int allocated by DB , with string keys, no db generation now
        this.Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); // default to db generated

        this.Property(t => t.RowVersion)
            .IsRequired()
            .IsFixedLength()
            .HasMaxLength(8)
            .IsRowVersion(); //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

Docu on the optimistic concurrency pattern

【讨论】:

  • MySQL 没有与 SQL Server 的 rowversion 等效的版本。 MS 已弃用 'timestamp' 支持 'rowversion'。
  • 遗憾的是,这不适用于 MySQL .NET 连接器 6.9.10。我刚刚提交了一个可以修复它的 PR。
【解决方案2】:

使用拦截器的解决方法

我看到有问题的 MySQL 连接器 bug 还没有修复(自 2014 年以来),我写了那个“解决方案”(我知道这很丑),直到他们修复它。

我创建一个 DBCommandInterceptor 并覆盖 ReaderExecuting 以将最后一个 WHERE 中的相等运算符 (=) 替换为不相等运算符 (&lt;&gt;),因为更新的模式类似于“ UPDATE ...; SELECT ... WHERE (row_version_field = @parameter)"

在下面的代码中,将正则表达式中的row_version 替换为您的行版本字段的名称。

public class ConcurrencyFixInterceptor : DbCommandInterceptor
{
    private static Regex concurrencyPattern = 
        new Regex(@"^UPDATE[\S\s]+SELECT[\S\s]+\(.?row_version.?\s(=)\s@[\w\d]+\)$", 
            RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        Match macth = concurrencyPattern.Match(command.CommandText);
        if (macth.Success)
        {
            command.CommandText = 
                command.CommandText.
                Remove(macth.Groups[1].Index, 1).
                Insert(macth.Groups[1].Index, "<>");
        }
        base.ReaderExecuting(command, interceptionContext);
    }

}

我在 MySQL 中使用带有 TIMESTAMP(5) 字段类型的行版本。

【讨论】:

    【解决方案3】:

    我刚刚提交了一个PR to MySQL .NET Connector v6.9.10,它为这个问题提供了解决方案。

    解决方法避免使用 TIMESTAMP 或 DATETIME 值来使用更安全的 BIGINT RowVersion 值执行乐观锁定,该值通过 BEFORE UPDATE 触发器递增。此修复程序现在将支持使用外部(非 EF)应用程序进行乐观锁定。如果我可以修复与 TIMESTAMP / DATETIME 相关的第二个错误,那么ConcurrencyCheck 也应该适用于这些类型。

    EF6:

    public class MyTable
    {
      [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
      public virtual int Id { get; set; }
    
      [Required, MaxLength(45)]
      public virtual string Name { get; set; }
    
      [ConcurrencyCheck, DatabaseGenerated(DatabaseGeneratedOption.Computed)]
      [Column(TypeName = "bigint")]
      public virtual long RowVersion { get; set; }
    }
    

    SQL:

    CREATE TABLE IF NOT EXISTS `mytable` (
        Id int(11) NOT NULL,
        Name varchar(45) NOT NULL,
        RowVersion bigint NOT NULL DEFAULT 0,
        PRIMARY KEY (`Id`)
    ) ENGINE=InnoDB
    
    CREATE TRIGGER `trg_mytable_before_update` 
    BEFORE UPDATE ON `mytable`
    FOR EACH ROW SET NEW.RowVersion = OLD.RowVersion + 1;
    

    TIMESTAMP 解决方案?

    我还在研究如何使用 TIMESTAMP 字段执行乐观锁定。

    首先,您需要使用更细粒度的时间戳值。

    因此,例如,如果您使用以下内容,您的时间戳值将被截断到最接近的秒数(对于乐观锁定来说不是很安全)。

    UpdatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIME ON UPDATE CURRENT_TIME
    

    您应该使用以下来记录微秒精度。

    UpdatedAt TIMESTAMP(6) NOT NULL DEFAULT NOW(6) ON UPDATE NOW(6)
    

    其次,我正在观察我在 MySQL .NET 连接器单元测试套件环境中重现的一个错误,并结合了我的 PR 补丁刚刚提交。 EF6 现在生成正确的乐观锁定 SQL 以执行 UPDATE,然后执行 SELECT(现已修复),返回更新的 TIMESTAMP 字段。但是,即使在 MySQL Workbench 中执行完全相同的 UPDATE 和 SELECT,MySQL 连接器也会返回零 TIMESTAMP (0000-00-00 00:00:00.000000),它返回一个有效的非零 TIMESTAMP 值。我观察到通过连接套接字读取的数据包返回字符串“0000-00-00 00:00:00.000000”,因此它可能在某种程度上与 MySQL 会话配置有关。欢迎提示!我目前正在使用 MySQL v5.6.26 (Windows) 进行测试。

    多个乐观锁字段

    在我们的例子中,我们有一个旧版 MS-Access 应用程序,它在大多数表中使用 TIMESTAMP 来执行乐观锁定。对于 MS-Access,这是一个方便的解决方案,因为它检测到任何 TIMESTAMP 列的存在,并在找到时自动将乐观锁定应用于该列。

    由于我们目前没有针对 TIMESTAMP 列使用 EF6 的乐观锁定,因此我们通过创建 BIGINT RowVersion 列在我们关心的每个表上添加了第二个乐观锁定列,因为该列通过 BEFORE INSERT 触发器递增。因此,现在对于每个 UPDATE,现有的 TIMESTAMP 列和新的 RowVersion 列都会更新,因此可以使用其中任何一个来检测更改。不理想但有效!

    【讨论】:

    • 希望我很快就会获得 EF6 / TIMESTAMP(6) 乐观锁定的 PR。我尚未检查 TIMESTAMP(6) 是否适用于 MS-Access。
    猜你喜欢
    • 2015-09-25
    • 1970-01-01
    • 2014-01-20
    • 1970-01-01
    • 2017-08-05
    • 2015-11-21
    • 2013-12-25
    • 2017-05-28
    • 1970-01-01
    相关资源
    最近更新 更多