我尝试在EF Core中使用SQLite的乐观并发检查。最简单的正向场景(即使没有并发本身)给我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列类型):
同时,如果我使用SQL Server(使用带注解的连接字符串+删除.HasDefaultValueSql("CURRENT_TIMESTAMP")
),示例将正常工作并更新DB中的时间戳。
已使用的软件包:
<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
}
}
5条答案
按热度按时间68de4m5k1#
EF Core SQLite提供程序在将标记为
byte[]
的[TimeStamp]
(或IsRowVersion()
)属性绑定到SQL查询参数时似乎未正确处理这些属性。它使用默认的byte[]
到hexstring
的转换,这种转换在本例中不适用-byte[]
实际上是string
。首先考虑将其报告给他们的问题跟踪者。然后,在问题得到解决之前(如果有),作为一种解决方法,您可以使用以下自定义
ValueConverter
:不幸的是,没有办法告诉EF Core仅将其用于参数,因此在将其分配为
.HasConversion(new SqliteTimestampConverter())
之后,现在db类型被视为string
,因此您需要添加.HasColumnType("BLOB")
。最终的工作Map为
您可以通过在
OnModelCreating
的末尾添加以下自定义SQLite RowVersion“约定”来避免所有这些问题:因此您的属性配置可以缩减为
或完全删除并替换为数据注解
0dxa2lsx2#
受此thread on GitHub和Ivan的答案的启发,我编写了此代码,以确保在我的单元测试中模拟SQL Server并发。
wfauudbj3#
这是因为您使用了
Guid
:此问题在Gitub中讨论和重现:
此处的错误是由ApplicationUser.ConcurrencyStamp属性引起的。标识中的ApplicationUser使用Guid类型的ConcurrencyStamp进行并发。创建新类时,它将该值设置为NewGuid()。当您创建新的ApplicationUser并将其状态设置为Modified时,EF Core在数据库中没有ConcurrencyStamp的相关数据。因此,它将使用在该项上设置的任何值(将为NewGuid())由于此值与数据库中的值不同,并且它用于update语句的where子句中,因此引发了异常,即0行在预期的1行修改时被修改。
使用并发令牌更新实体时,您无法创建新对象并直接发送更新。您必须从数据库中检索记录(以便获得ConcurrencyStamp的值),然后更新记录并调用SaveChanges。由于ApplicationUser.ConcurrencyStamp是客户端并发令牌,因此您还需要在更新记录时生成NewGuid()。因此它可以更新数据库中的值。
在此查找有关如何处理
ApplicationUser.ConcurrencyStamp
的更多信息。fhity93d4#
到目前为止,我一直在使用Ivan's answer,并取得了巨大的成功。然而,当我更新到EntityFrameworkCore 3.1时,我开始收到以下警告:
实体类型“{entity name}”上的属性“{column name}”是具有值转换器但没有值比较器的集合或枚举类型。请设置值比较器以确保正确比较集合/枚举元素。
为了解决这个问题,我通过添加以下内容增强了他的解决方案:
(基于对GitHub问题的回复)
7uhlpewt5#
我无法使它与
CURRENT_TIMESTAMP
一起工作,因为它以在SQLite和EF的DB浏览器中显示的字符串结束。具有long
类型的实体属性导致2022
值,并阻止我保存列,因为很明显2022
!=2022-12-09 14:03:14
。它也只有秒,这对并发性不利。所以我决定采用另一种解决方案
byte[]
+SetDefaultValueSql("randomblob(8)")
+更新触发器属性本身表示常规版本列
现在,我们需要添加一些反射魔法,以便在插入时一次性为所有实体分配默认值:
还有最后一步:我们需要强制数据库在行更新时更新
Version
列。我们需要一个触发器,但我不希望在迁移过程中进行任何手动操作,因此下面是基于迁移和播种过程的自动化:因此,我们获取现有触发器的列表,然后遍历所有
DBSet<>
属性并插入缺失的触发器。但SQLite方面存在限制。
EF Core在更新期间发出此请求:
因此,我们的想法是在列更新后使用新的
RETURNING
子句返回更新后的Version
值。https://www3.sqlite.org/lang_returning.html不幸的是:
RETURNING子句只返回由DELETE、INSERT或UPDATE语句直接修改的行。RETURNING子句不报告由外键约束条件或触发器导致的任何其他数据库更改。
这意味着在
SaveChanges()
之后返回的项将具有与之前相同的Version
值,因为SQLite返回的是旧值而不是新值。因此,您将无法再次更改和保存它。您可以使用以下行轻松检查它:var sqlQueryRaw = BitConverter.ToString(dbContext.Database.SqlQueryRaw<byte[]>("UPDATE PrinterOperations SET CommandJson = '111111' WHERE Id = '73cf2572-4ba3-4693-922c-b062be47707c' RETURNING Version").ToArray().First());
只要运行几次