我尝试在带有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列类型):
如果我同时使用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
}
}
答案 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问题)