fla*_*am3 4 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; }
}
Run Code Online (Sandbox Code Playgroud)
内容:
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");
}
}
Run Code Online (Sandbox Code Playgroud)
样品:
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'
}
}
}
Run Code Online (Sandbox Code Playgroud)
我怀疑这与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;
Run Code Online (Sandbox Code Playgroud)
但是列类型对于Id和Timestamp都是BLOB(SQLite不提供UUID和timestamp列类型):
同时,如果我使用SQL Server(使用带注释的连接字符串+ remove .HasDefaultValueSql("CURRENT_TIMESTAMP")),则示例可以正常工作并更新数据库中的时间戳。
二手包装:
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.4" />
Run Code Online (Sandbox Code Playgroud)
我是否将并发检查模型配置为错误?这让我发疯,因为我无法在最简单的情况下使用它。
更新:我最终如何使其工作。这里仅显示想法,但可能对任何人都有帮助:
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
}
}
Run Code Online (Sandbox Code Playgroud)
在将它们绑定到SQL查询参数时,似乎EF Core SQLite提供程序无法正确处理[TimeStamp](或IsRowVersion())标记的byte[]属性。它使用默认byte[]的十六进制 string转换,在这种情况下不适用- byte[]实际上是一个string。
首先考虑将其报告给他们的问题跟踪器。然后,直到解决(如果有),作为一种解决方法,您可以使用以下定制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))
}
Run Code Online (Sandbox Code Playgroud)
不幸的是,没有办法告诉EF Core仅将其用于参数,因此在为其分配后.HasConversion(new SqliteTimestampConverter()),现在考虑使用db类型string,因此您需要添加.HasColumnType("BLOB")。
最终的工作映射为
modelBuilder.Entity<Blog>()
.Property(p => p.Timestamp)
.IsRowVersion()
.HasConversion(new SqliteTimestampConverter())
.HasColumnType("BLOB")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
Run Code Online (Sandbox Code Playgroud)
您可以通过在末尾添加以下自定义SQLite RowVersion“常规”来避免所有这些情况OnModelCreating:
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";
}
}
Run Code Online (Sandbox Code Playgroud)
因此您的媒体资源配置可以调整为
modelBuilder.Entity<Blog>()
.Property(p => p.Timestamp)
.IsRowVersion();
Run Code Online (Sandbox Code Playgroud)
或完全删除并替换为数据注释
public class Blog
{
public Guid Id { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
Run Code Online (Sandbox Code Playgroud)