DbUpdateConcurrencyException using Entity Framework 6 with MySql

杀马特。学长 韩版系。学妹 提交于 2019-11-29 15:41:17

You should be trying to use the DB Timestamp / Rowversion feature. In EF you declare a ByteArray and nominate it as the Concurrency check field. DB sets the value on creation. All subsequent updates can check the value hasnt changed DB updates rowversion as appropriate. This approach works on SQL server. It should behave the same way on MYSql.

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

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

    }

or via fluent if you like // Primary Key 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

Workarround with Interceptor

I see that MySQL Connector bug pointed in question do not have a fix yet (since 2014) and I wrote that "solution" (I know this is ugly) until the they fix it.

I create an DBCommandInterceptor and override the ReaderExecuting to replace the equal operator (=) in last WHERE to a not equal operator (<>) because the pattern for the update is something like "UPDATE ...; SELECT ... WHERE (row_version_field = @parameter)"

In the code bellow replace row_version in regular expression with the name of your row version field.

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);
    }

}

I use a row version in MySQL with a TIMESTAMP(5) field type.

I have just submitted a PR to MySQL .NET Connector v6.9.10 that provides a workaround solution for this issue.

The workaround avoids use of TIMESTAMP or DATETIME values to perform optimistic locking using a safer BIGINT RowVersion value that is incremented via a BEFORE UPDATE trigger. This fix will now support optimistic locking with an external (non-EF) application. If I can fix a 2nd bug related to TIMESTAMP / DATETIME then ConcurrencyCheck should work with these types as well.

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 Solution?

I'm also investigating how to performing optimistic locking with a TIMESTAMP field.

Firstly, you need to use a more fine grained timestamp value.

So for example if you use the following, your timestamp value will be truncated to the nearest second (not very safe for optimistic locking).

UpdatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIME ON UPDATE CURRENT_TIME

Instead you should use following to record microsecond precision.

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

Secondly, I'm observing a bug that I'm reproducing within the environment of the MySQL .NET Connector unit test suite combined with the PR patch I've just submitted. EF6 now generates the correct optimistic locking SQL to perform an UPDATE followed by the SELECT (now fixed) that returns the updated TIMESTAMP field. However the MySQL connector returns a zero TIMESTAMP (0000-00-00 00:00:00.000000) even though executing the exact same UPDATE and SELECT in MySQL Workbench it returns a valid non-zero TIMESTAMP value. I've observed the packets read via the connection socket return the string '0000-00-00 00:00:00.000000' so its probably related to the MySQL session configuration in some way. Hints welcome! I'm currently testing this with MySQL v5.6.26 (Windows).

Multiple optimistic lock fields

In our case, we have a legacy MS-Access app that uses a TIMESTAMP in most tables to perform optimistic locking. This is a convenient solution for MS-Access as it detects the presence of any TIMESTAMP column and automatically applies optimistic locking to this column when it finds one.

Since we currently don't have optimistic locking working with EF6 for TIMESTAMP columns we've added a second optimistic lock column on each table we care about by creating a BIGINT RowVersion column as that is incremented via a BEFORE INSERT trigger. So now for each UPDATE both the existing TIMESTAMP column and the new RowVersion column are update so either can be used to detect a change. Not ideal but it works!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!