c# - 使用 SQLite 更新 EF Core 应用程序中的实体会出现 DbUpdateConcurrencyException

标签 c# sqlite entity-framework-core

我尝试在 EF Core 和 SQLite 中使用乐观并发检查。 最简单的积极场景(即使没有并发本身)给了我 Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: '数据库操作预计会影响 1 行但实际上影响了 0 行。加载实体后数据可能已被修改或删除

实体:

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

最佳答案

看起来 EF Core SQLite 提供程序在绑定(bind)时未正确处理 [TimeStamp](或 IsRowVersion())标记的 byte[] 属性SQL 查询参数。它使用默认的 byte[]hex string 转换,这在这种情况下不适用 - byte[] 实际上一个字符串

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

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

遗憾的是,无法告诉 EF Core 仅将其用于参数,因此在使用 .HasConversion(new SqliteTimestampConverter()) 分配后,现在数据库类型被视为 string ,所以需要添加.HasColumnType("BLOB")

最终的工作映射是

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

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

if (Database.IsSqlite())
{
    var timestampProperties = modelBuilder.Model
        .GetEntityTypes()
        .SelectMany(t => t.GetProperties())
        .Where(p => p.ClrType == typeof(byte[])
            && p.ValueGenerated == ValueGenerated.OnAddOrUpdate
            && p.IsConcurrencyToken);

    foreach (var property in timestampProperties)
    {
        property.SetValueConverter(new SqliteTimestampConverter());
        property.Relational().DefaultValueSql = "CURRENT_TIMESTAMP";
    }
}

因此您的属性配置可以精简为

modelBuilder.Entity<Blog>()
    .Property(p => p.Timestamp)
    .IsRowVersion();

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

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

关于c# - 使用 SQLite 更新 EF Core 应用程序中的实体会出现 DbUpdateConcurrencyException,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52684458/

相关文章:

c# - 如何在 ServiceStack 中传输大型多千兆文件?

c# - 在 Entity Framework 中动态传递要在运行时选择的列名称

java - 使用 SQLite 从数据库中选择 - 'No such column'

android - Android 上的 Ionic 应用程序文件夹在哪里?

view - 如何在带有 Entity Framework Core 1.0 (EF7) 的脚手架 DbContext 中使用数据库 View

c# - ASP.NET vNext 和 Ninject 在两个服务的构造函数之间检测到循环依赖

c# - Entity Framework 集成测试无法运行多个单独通过的测试 [Resharper、NUnit、EF6]

php - 在 SQLite 中选择和项目的当前、上一个和下一个值的最有效方法

asp.net-core - EF Core 中的 AddAsync() 与 Add()

c# - 在 Docker 镜像中使用 ASP.Net Core 时应用 Entity Framework 迁移