我在使用EF6和MySQL进行并发检查时遇到了麻烦。
我遇到的问题是,当我尝试将数据保存到数据库时,我得到了一个并发异常。如果检查输出到控制台的sql,它会尝试使用where子句中的旧值从数据库中查询并发字段。因为此字段已由数据库更新。
环境:
安装Nuget包:
演示数据库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。
答案 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(); //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
答案 1 :(得分:0)
我看到有问题的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
也应该适用于这些类型。
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; }
}
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字段执行乐观锁定。
首先 ,您需要使用更细粒度的时间戳值。
因此,例如,如果您使用以下内容,您的时间戳值将被截断为最接近的秒(对乐观锁定来说不是很安全)。
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列都会更新,因此可以使用它们来检测更改。不理想,但它的工作原理!