在不更新行版本的情况下检查实体的并发性
Checking Concurrency on an Entity without updating the Row Version
我有一个 父实体 ,我需要进行 并发检查 (如下注释)
[Timestamp]
public byte[] RowVersion { get; set; }
我有一堆 客户端进程 从这个 父实体 [=83= 访问 只读 值],主要是更新它的子实体。
约束条件
客户端不应干扰彼此的工作,(例如,更新子记录不应在父实体[上引发并发异常).
我有一个服务器进程,执行更新这个父实体,在这种情况下,如果父实体已更改,则客户端进程需要抛出。
注意 : 客户端的并发检查是牺牲性的,服务器的工作流是关键任务.
问题
我需要检查(从客户端进程)父实体是否已更改而不更新父实体实体的行版本。
在 EF:
中对 父实体 进行并发检查非常容易
// Update the row version's original value
_db.Entry(dbManifest)
.Property(b => b.RowVersion)
.OriginalValue = dbManifest.RowVersion; // the row version the client originally read
// Mark the row version as modified
_db.Entry(dbManifest)
.Property(x => x.RowVersion)
.IsModified = true;
IsModified = true
是交易破坏者,因为它强制更改行版本。或者,在上下文中,来自客户端进程的此检查将导致 父实体 中的行版本更改,这会不必要地干扰其他 客户端进程' 工作流程。
解决方法:我可能会将来自客户端进程的 SaveChanges
包装在一个事务中,然后是后续的依次读取父实体的行版本,如果行版本已更改则回滚。
总结
是否有 开箱即用的方法 Entity Framework 我可以 SaveChanges
(在 客户端进程 中 子实体 )还检查 父实体的行版本 是否有已更改(未更新 父实体行版本)。
那么,您需要做的是在写入子实体时检查父实体的并发令牌(时间戳)。唯一的挑战是父时间戳不在子实体中。
您没有明确说明,但我假设您使用的是 EF Core。
查看 https://docs.microsoft.com/en-us/ef/core/saving/concurrency,如果 UPDATE 或 DELETE 影响零行,EF Core 似乎会抛出并发异常。为了实现并发测试,EF 添加了一个 WHERE 子句来测试并发标记,然后测试 UPDATE 或 DELETE 是否影响了正确的行数。
您可以尝试向 UPDATE 或 DELETE 添加一个额外的 WHERE 子句来测试父级的 RowVersion 的值。我认为您可以使用 System.Diagnostics.DiagnosticListener class 拦截 EF Core 2 来做到这一点。在 https://weblogs.asp.net/ricardoperes/interception-in-entity-framework-core and a discussion at . Evidently EF Core 3 (I think it is coming in September/October) will include an interception mechanism similar to that which was in EF pre-Core, see https://github.com/aspnet/EntityFrameworkCore/issues/15066
上有一篇关于它的文章
希望这对你有用。
有一个非常简单的解决方案,“out-of-2-boxes”,但它需要进行两次修改我不确定您是否可以或愿意:
- 创建包含
ParentRowVersion
列 的 updatable 子视图 table
- 将子实体映射到此视图
让我来展示一下这是如何工作的。一切都非常简单。
数据库模型:
CREATE TABLE [dbo].[Parent]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Parent] ADD CONSTRAINT [PK_Parent] PRIMARY KEY CLUSTERED ([ID]) ON [PRIMARY]
CREATE TABLE [dbo].[Child]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL,
[ParentID] [int] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Child] ADD CONSTRAINT [PK_Child] PRIMARY KEY CLUSTERED ([ID]) ON [PRIMARY]
GO
CREATE VIEW [dbo].[ChildView]
WITH SCHEMABINDING
AS
SELECT Child.ID
, Child.Name
, Child.ParentID
, Child.RowVersion
, p.RowVersion AS ParentRowVersion
FROM dbo.Child
INNER JOIN dbo.Parent p ON p.ID = Child.ParentID
视图已更新table 因为它符合 conditions for Sql Server views to be updatable.
数据
SET IDENTITY_INSERT [dbo].[Parent] ON
INSERT INTO [dbo].[Parent] ([ID], [Name]) VALUES (1, N'Parent1')
SET IDENTITY_INSERT [dbo].[Parent] OFF
SET IDENTITY_INSERT [dbo].[Child] ON
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (1, N'Child1.1', 1)
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (2, N'Child1.2', 1)
SET IDENTITY_INSERT [dbo].[Child] OFF
Class 型号
public class Parent
{
public Parent()
{
Children = new HashSet<Child>();
}
public int ID { get; set; }
public string Name { get; set; }
public byte[] RowVersion { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public int ID { get; set; }
public string Name { get; set; }
public byte[] RowVersion { get; set; }
public int ParentID { get; set; }
public Parent Parent { get; set; }
public byte[] ParentRowVersion { get; set; }
}
上下文
public class TestContext : DbContext
{
public TestContext(string connectionString) : base(connectionString){ }
public DbSet<Parent> Parents { get; set; }
public DbSet<Child> Children { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Parent>().Property(e => e.RowVersion).IsRowVersion();
modelBuilder.Entity<Child>().ToTable("ChildView");
modelBuilder.Entity<Child>().Property(e => e.ParentRowVersion).IsRowVersion();
}
}
汇集在一起
这段代码更新了一个 Child
而一个假的并发用户更新了它的 Parent
:
using (var db = new TestContext(connString))
{
var child = db.Children.Find(1);
// Fake concurrent update of parent.
db.Database.ExecuteSqlCommand("UPDATE dbo.Parent SET Name = Name + 'x' WHERE ID = 1");
child.Name = child.Name + "y";
db.SaveChanges();
}
现在 SaveChanges
抛出所需的 DbUpdateConcurrencyException
。当父更新被注释掉时,子更新成功。
我认为这种方法的优点是它非常独立于数据访问库。您只需要一个支持乐观并发的 ORM。将来搬到 EF-core 不会有问题。
从一个项目到另一个项目,我在广泛的平台(不仅是 .Net)上遇到了这个问题。
从架构的角度来看,我可以提出几个并非 EntityFramework 独有的决定。 (至于我#2更好)
选项 1 实现乐观锁定方法。一般的想法听起来像:“让我们更新客户端,然后检查父级的状态”。您已经提到了“使用事务”的想法,但是乐观锁定可以减少保留父实体所需的时间。类似于:
var expectedVersion = _db.Parent...First().RowVersion;
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required))
{
//modify Client entity there
...
//now make second check of Parent version
if( expectedVersion != _db.Parent...First().RowVersion )
throw new Exception(...);
_db.SaveChanges();
}
注意!根据 SQL 服务器设置(隔离级别),您可能需要应用到父实体 select-for-update 请查看如何操作。 How to implement Select For Update in EF Core
选项 2 至于我更好的方法而不是 EF 使用显式 SQL 类似的东西:
UPDATE
SET Client.BusinessValue = :someValue -- changes of client
FROM Client, Parent
WHERE Client.Id = :clientToChanges -- restrict updates by criteria
AND Client.ParentId = Parent.Id -- join with Parent entity
AND Parent.RowVersion = :expectedParent
在 .Net 代码中进行此查询后,您需要检查是否恰好有 1 行受到影响(0 表示 Parent.Rowversion
已更改)
if(_db.ExecuteSqlCommand(sql) != 1 )
throw new Exception();
也尝试在其他 DB-table 的帮助下分析“全局锁定”设计模式。您可以在那里阅读有关此方法的信息 http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
我有一个 父实体 ,我需要进行 并发检查 (如下注释)
[Timestamp]
public byte[] RowVersion { get; set; }
我有一堆 客户端进程 从这个 父实体 [=83= 访问 只读 值],主要是更新它的子实体。
约束条件
客户端不应干扰彼此的工作,(例如,更新子记录不应在父实体[上引发并发异常).
我有一个服务器进程,执行更新这个父实体,在这种情况下,如果父实体已更改,则客户端进程需要抛出。
注意 : 客户端的并发检查是牺牲性的,服务器的工作流是关键任务.
问题
我需要检查(从客户端进程)父实体是否已更改而不更新父实体实体的行版本。
在 EF:
中对 父实体 进行并发检查非常容易// Update the row version's original value
_db.Entry(dbManifest)
.Property(b => b.RowVersion)
.OriginalValue = dbManifest.RowVersion; // the row version the client originally read
// Mark the row version as modified
_db.Entry(dbManifest)
.Property(x => x.RowVersion)
.IsModified = true;
IsModified = true
是交易破坏者,因为它强制更改行版本。或者,在上下文中,来自客户端进程的此检查将导致 父实体 中的行版本更改,这会不必要地干扰其他 客户端进程' 工作流程。
解决方法:我可能会将来自客户端进程的 SaveChanges
包装在一个事务中,然后是后续的依次读取父实体的行版本,如果行版本已更改则回滚。
总结
是否有 开箱即用的方法 Entity Framework 我可以 SaveChanges
(在 客户端进程 中 子实体 )还检查 父实体的行版本 是否有已更改(未更新 父实体行版本)。
那么,您需要做的是在写入子实体时检查父实体的并发令牌(时间戳)。唯一的挑战是父时间戳不在子实体中。
您没有明确说明,但我假设您使用的是 EF Core。
查看 https://docs.microsoft.com/en-us/ef/core/saving/concurrency,如果 UPDATE 或 DELETE 影响零行,EF Core 似乎会抛出并发异常。为了实现并发测试,EF 添加了一个 WHERE 子句来测试并发标记,然后测试 UPDATE 或 DELETE 是否影响了正确的行数。
您可以尝试向 UPDATE 或 DELETE 添加一个额外的 WHERE 子句来测试父级的 RowVersion 的值。我认为您可以使用 System.Diagnostics.DiagnosticListener class 拦截 EF Core 2 来做到这一点。在 https://weblogs.asp.net/ricardoperes/interception-in-entity-framework-core and a discussion at
希望这对你有用。
有一个非常简单的解决方案,“out-of-2-boxes”,但它需要进行两次修改我不确定您是否可以或愿意:
- 创建包含
ParentRowVersion
列 的 updatable 子视图 table
- 将子实体映射到此视图
让我来展示一下这是如何工作的。一切都非常简单。
数据库模型:
CREATE TABLE [dbo].[Parent]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Parent] ADD CONSTRAINT [PK_Parent] PRIMARY KEY CLUSTERED ([ID]) ON [PRIMARY]
CREATE TABLE [dbo].[Child]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL,
[ParentID] [int] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Child] ADD CONSTRAINT [PK_Child] PRIMARY KEY CLUSTERED ([ID]) ON [PRIMARY]
GO
CREATE VIEW [dbo].[ChildView]
WITH SCHEMABINDING
AS
SELECT Child.ID
, Child.Name
, Child.ParentID
, Child.RowVersion
, p.RowVersion AS ParentRowVersion
FROM dbo.Child
INNER JOIN dbo.Parent p ON p.ID = Child.ParentID
视图已更新table 因为它符合 conditions for Sql Server views to be updatable.
数据
SET IDENTITY_INSERT [dbo].[Parent] ON
INSERT INTO [dbo].[Parent] ([ID], [Name]) VALUES (1, N'Parent1')
SET IDENTITY_INSERT [dbo].[Parent] OFF
SET IDENTITY_INSERT [dbo].[Child] ON
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (1, N'Child1.1', 1)
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (2, N'Child1.2', 1)
SET IDENTITY_INSERT [dbo].[Child] OFF
Class 型号
public class Parent
{
public Parent()
{
Children = new HashSet<Child>();
}
public int ID { get; set; }
public string Name { get; set; }
public byte[] RowVersion { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public int ID { get; set; }
public string Name { get; set; }
public byte[] RowVersion { get; set; }
public int ParentID { get; set; }
public Parent Parent { get; set; }
public byte[] ParentRowVersion { get; set; }
}
上下文
public class TestContext : DbContext
{
public TestContext(string connectionString) : base(connectionString){ }
public DbSet<Parent> Parents { get; set; }
public DbSet<Child> Children { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Parent>().Property(e => e.RowVersion).IsRowVersion();
modelBuilder.Entity<Child>().ToTable("ChildView");
modelBuilder.Entity<Child>().Property(e => e.ParentRowVersion).IsRowVersion();
}
}
汇集在一起
这段代码更新了一个 Child
而一个假的并发用户更新了它的 Parent
:
using (var db = new TestContext(connString))
{
var child = db.Children.Find(1);
// Fake concurrent update of parent.
db.Database.ExecuteSqlCommand("UPDATE dbo.Parent SET Name = Name + 'x' WHERE ID = 1");
child.Name = child.Name + "y";
db.SaveChanges();
}
现在 SaveChanges
抛出所需的 DbUpdateConcurrencyException
。当父更新被注释掉时,子更新成功。
我认为这种方法的优点是它非常独立于数据访问库。您只需要一个支持乐观并发的 ORM。将来搬到 EF-core 不会有问题。
从一个项目到另一个项目,我在广泛的平台(不仅是 .Net)上遇到了这个问题。 从架构的角度来看,我可以提出几个并非 EntityFramework 独有的决定。 (至于我#2更好)
选项 1 实现乐观锁定方法。一般的想法听起来像:“让我们更新客户端,然后检查父级的状态”。您已经提到了“使用事务”的想法,但是乐观锁定可以减少保留父实体所需的时间。类似于:
var expectedVersion = _db.Parent...First().RowVersion;
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required))
{
//modify Client entity there
...
//now make second check of Parent version
if( expectedVersion != _db.Parent...First().RowVersion )
throw new Exception(...);
_db.SaveChanges();
}
注意!根据 SQL 服务器设置(隔离级别),您可能需要应用到父实体 select-for-update 请查看如何操作。 How to implement Select For Update in EF Core
选项 2 至于我更好的方法而不是 EF 使用显式 SQL 类似的东西:
UPDATE
SET Client.BusinessValue = :someValue -- changes of client
FROM Client, Parent
WHERE Client.Id = :clientToChanges -- restrict updates by criteria
AND Client.ParentId = Parent.Id -- join with Parent entity
AND Parent.RowVersion = :expectedParent
在 .Net 代码中进行此查询后,您需要检查是否恰好有 1 行受到影响(0 表示 Parent.Rowversion
已更改)
if(_db.ExecuteSqlCommand(sql) != 1 )
throw new Exception();
也尝试在其他 DB-table 的帮助下分析“全局锁定”设计模式。您可以在那里阅读有关此方法的信息 http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html