使用带有MySql的Entity Framework 6的DbUpdateConcurrencyException

时间:2014-07-09 04:00:27

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

我在使用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

3 个答案:

答案 0 :(得分:1)

您应该尝试使用DB Timestamp / Rowversion功能。 在EF中,您声明一个ByteArray并将其指定为Concurrency check字段。 DB在创建时设置值。所有后续更新都可以检查值是否已更改 DB会根据需要更新rowversion。此方法适用于SQL Server。 它应该在MYSql上的行为方式相同。

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

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

    }

或者如果你愿意,可以通过流利     // 首要的关键             this.HasKey(t =&gt; 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

答案 1 :(得分:0)

与拦截器

的Workarround

我看到有问题的MySQL Connector bug还没有修复(自2014年起),我写了“解决方案”(我知道这很难看),直到他们修复它。

我创建了一个DBCommandInterceptor并覆盖了ReaderExecuting,将最后=中的等号运算符(WHERE)替换为不等运算符(<>)因为更新的模式类似于“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)字段类型的行版本。

答案 2 :(得分:0)

我刚刚提交了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 Connector单元测试套件环境中复制的错误以及PR补丁我已经刚刚提交。 EF6现在生成正确的乐观锁定SQL以执行UPDATE,然后执行返回更新的TIMESTAMP字段的SELECT(现在已修复)。然而,MySQL连接器返回零TIMESTAMP(毛派00:00:00.000000),即使在MySQL Workbench中执行完全相同的UPDATE和SELECT,它也会返回有效的非零TIMESTAMP值。我观察到通过连接套接字读取的数据包返回字符串'0000-00-00 00:00:00.000000',因此它可能以某种方式与MySQL会话配置相关。提示欢迎!我目前正在使用MySQL v5.6.26(Windows)进行测试。

多个乐观锁定字段

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

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