使用SQLite更新EF Core应用程序中的实体会提供DbUpdateConcurrencyException

时间:2018-10-07 00:27:35

标签: c# sqlite entity-framework-core

我尝试在带有SQLite的EF Core中使用开放式并发检查。 最简单的积极情景(即使没有并发本身)也给了我 Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: 'Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded

实体:

public class Blog
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public byte[] Timestamp { get; set; }
}

上下文:

internal class Context : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite(@"Data Source=D:\incoming\test.db");
        ///optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasKey(p => p.Id);

        modelBuilder.Entity<Blog>()
            .Property(p => p.Timestamp)
            .IsRowVersion()
            .HasDefaultValueSql("CURRENT_TIMESTAMP");
    }
}

示例:

internal class Program
{
    public static void Main(string[] args)
    {
        var id = Guid.NewGuid();
        using (var db = new Context())
        {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();
            db.Blogs.Add(new Blog { Id = id, Name = "1" });
            db.SaveChanges();
        }

        using (var db = new Context())
        {
            var existing = db.Blogs.Find(id);
            existing.Name = "2";
            db.SaveChanges(); // Exception thrown: 'Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException'
        }

    }
}

我怀疑这与EF和SQLite之间的数据类型有关。 日志记录为我提供了有关更新的以下查询:

Executing DbCommand [Parameters=[@p1='2bcc42f5-5fd9-4cd6-b0a0-d1b843022a4b' (DbType = String), @p0='2' (Size = 1), @p2='0x323031382D31302D30372030393A34393A3331' (Size = 19) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1 AND "Timestamp" = @p2;

但是列类型对于Id和Timestamp都是BLOB(SQLite不提供UUID和timestamp列类型):

enter image description here


如果我同时使用SQL Server(使用带注释的连接字符串+删除.HasDefaultValueSql("CURRENT_TIMESTAMP")),则示例可以正常工作并更新数据库中的时间戳。

使用过的软件包:

<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.4" />

我是否为并发检查配置了模型? 这让我发疯,因为我无法在最简单的情况下使用它。


更新:我最终是如何工作的。这里只显示了想法,但可能对任何人都有帮助

public class Blog
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public long Version { get; set; }
}

internal class Context : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite(@"Data Source=D:\incoming\test.db");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasKey(p => p.Id);

        modelBuilder.Entity<Blog>()
            .Property(p => p.Version)
            .IsConcurrencyToken();
    }
}

internal class Program
{
    public static void Main(string[] args)
    {
        var id = Guid.NewGuid();
        long ver;
        using (var db = new Context())
        {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();
            var res = db.Blogs.Add(new Blog { Id = id, Name = "xxx", Version = DateTime.Now.Ticks});
            db.SaveChanges();
        }

        using (var db = new Context())
        {
            var existing = db.Blogs.Find(id);
            existing.Name = "yyy";
            existing.Version = DateTime.Now.Ticks;
            db.SaveChanges(); // success
        }

        using (var db = new Context())
        {
            var existing = db.Blogs.Find(id);
            existing.Name = "zzz";
            existing.Version = DateTime.Now.Ticks;
            db.SaveChanges(); // success
        }

        var t1 = Task.Run(() =>
        {
            using (var db = new Context())
            {
                var existing = db.Blogs.Find(id);
                existing.Name = "yyy";
                existing.Version = DateTime.Now.Ticks;
                db.SaveChanges();
            }
        });

        var t2 = Task.Run(() =>
        {
            using (var db = new Context())
            {
                var existing = db.Blogs.Find(id);
                existing.Name = "zzz";
                existing.Version = DateTime.Now.Ticks;
                db.SaveChanges();
            }
        });

        Task.WaitAll(t1, t2); // one of the tasks throws DbUpdateConcurrencyException
    }
}

4 个答案:

答案 0 :(得分:4)

当将EF Core SQLite提供程序绑定到SQL查询参数时,它们似乎无法正确处理标记为<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>jsTree test</title> <!-- 1 load Font-awesome css> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <!-- 2 load the theme CSS file --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/themes/default/style.min.css" /> </head> <body> <!-- 3 setup a container element --> <div id="jstree"> </div> <!-- 4 include the jQuery library --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script> <!-- 5 include the minified jstree source --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/jstree.min.js"></script> <script> $(function () { $('#jstree').jstree({ "core" : { "data" : [ { "data" : "A node", "metadata" : { id : 23 }, "children" : [ "Child 1", "A Child 2" ] }, { "attr" : { "id" : "li.node.id1" }, "data" : { "title" : "Long format demo", "attr" : { "href" : "#" } } } ] }, "types" : { "default" : { "icon" : "fa fa-cloud" } }, "plugins" : [ "themes", "json_data", "ui", "types"] }); }); </script> </body> </html> 的{​​{1}}(或[TimeStamp])标记为IsRowVersion()的属性。它使用默认的byte[]进行十六进制 byte[]转换,这种转换在这种情况下不适用-string实际上一个{{ 1}}。

首先考虑将其报告给他们的问题跟踪器。然后,直到解决(如果有),作为一种解决方法,您可以使用以下自定义ValueConverter

byte[]

不幸的是,没有办法告诉EF Core仅将其用于参数,因此在用string分配它之后,现在数据库类型被认为是class SqliteTimestampConverter : ValueConverter<byte[], string> { public SqliteTimestampConverter() : base( v => v == null ? null : ToDb(v), v => v == null ? null : FromDb(v)) { } static byte[] FromDb(string v) => v.Select(c => (byte)c).ToArray(); // Encoding.ASCII.GetString(v) static string ToDb(byte[] v) => new string(v.Select(b => (char)b).ToArray()); // Encoding.ASCII.GetBytes(v)) } ,因此您需要添加{{1 }}。

最终的工作映射是

.HasConversion(new SqliteTimestampConverter())

您可以通过在string的末尾添加以下自定义SQLite RowVersion“常规”来避免所有这些情况:

.HasColumnType("BLOB")

因此您的媒体资源配置可以精简为

    modelBuilder.Entity<Blog>()
        .Property(p => p.Timestamp)
        .IsRowVersion()
        .HasConversion(new SqliteTimestampConverter())
        .HasColumnType("BLOB")
        .HasDefaultValueSql("CURRENT_TIMESTAMP");

或完全删除并替换为数据注释

OnModelCreating

答案 1 :(得分:1)

这是因为您使用Guid

public Guid Id { get; set; }

此问题已在Gitub中进行了讨论和复制:

  

此处的错误归因于ApplicationUser.ConcurrencyStamp属性。   身份中的ApplicationUser使用类型为Guid的ConcurrencyStamp   并发。创建新类时,它将值设置为NewGuid()。   当您像这样创建新的ApplicationUser并将其状态设置为   修改后的EF Core没有有关什么是ConcurrencyStamp的数据   数据库。因此它将使用项目上设置的任何值   (将为NewGuid()),因为此值与   数据库,用于更新语句的where子句,异常   抛出0行修改为预期的1。

     

使用并发令牌更新实体时,无法创建新实体   反对并直接发送更新。您必须从以下位置检索记录   数据库(以便您具有ConcurrencyStamp的值),然后更新   记录并调用SaveChanges。自从   ApplicationUser.ConcurrencyStamp是您的客户端并发令牌   还需要在更新记录时生成NewGuid()。这样可以   更新数据库中的值。

查找有关如何处理ApplicationUser.ConcurrencyStamp here的更多信息。

答案 2 :(得分:0)

受此thread on GitHub和Ivan的回答启发,我编写了这段代码,以确保在单元测试中模仿SQL Server并发性。

var connection = new SqliteConnection("DataSource=:memory:");

var options = new DbContextOptionsBuilder<ActiveContext>()
               .UseSqlite(connection)
               .Options;

var ctx = new ActiveContext(options);

if (connection.State != System.Data.ConnectionState.Open)
{
    connection.Open();

    ctx.Database.EnsureCreated();

    var tables = ctx.Model.GetEntityTypes();

    foreach (var table in tables)
    {
        var props = table.GetProperties()
                        .Where(p => p.ClrType == typeof(byte[])
                        && p.ValueGenerated == Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate
                        && p.IsConcurrencyToken);

        var tableName = table.Relational().TableName;

        foreach (var field in props)
        {
            string[] SQLs = new string[] {
                $@"CREATE TRIGGER Set{tableName}_{field.Name}OnUpdate
                AFTER UPDATE ON {tableName}
                BEGIN
                    UPDATE {tableName}
                    SET RowVersion = randomblob(8)
                    WHERE rowid = NEW.rowid;
                END
                ",
                $@"CREATE TRIGGER Set{tableName}_{field.Name}OnInsert
                AFTER INSERT ON {tableName}
                BEGIN
                    UPDATE {tableName}
                    SET RowVersion = randomblob(8)
                    WHERE rowid = NEW.rowid;
                END
                "
            };

            foreach (var sql in SQLs)
            {
                using (var command = connection.CreateCommand())
                {
                    command.CommandText = sql;
                    command.ExecuteNonQuery();
                }
            }
        }
    }
}

答案 3 :(得分:0)

到目前为止,我一直在使用Ivan's answer取得了巨大的成功。但是,当我更新到EntitiyFrameworkCore 3.1时,我开始收到此警告:

实体类型“ {实体名称}”上的属性“ {列名称}”是具有值转换器但没有值比较器的集合或枚举类型。设置一个值比较器,以确保正确比较收集/枚举元素。

为解决这个问题,我通过添加以下内容增强了他的解决方案:

property.SetValueComparer(new ValueComparer<byte[]>(
    (c1, c2) => c1.SequenceEqual(c2),
    c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
    c => c.ToArray()));

(基于a response(基于GitHub问题)