在不更新行版本的情况下检查实体的并发性
Posted
技术标签:
【中文标题】在不更新行版本的情况下检查实体的并发性【英文标题】:Checking Concurrency on an Entity without updating the Row Version 【发布时间】:2019-11-24 16:30:02 【问题描述】:我有一个父实体,我需要做一个并发检查(如下注释)
[Timestamp]
public byte[] RowVersion get; set;
我有一堆客户端进程,它们从这个父实体访问只读值,主要是更新它的子实体。
约束
客户端不应干扰彼此的工作(例如,更新子记录不应在父实体上引发并发异常)。
我有一个服务器进程,它会 更新这个父实体,在这种情况下如果 父实体 已更改,客户端进程需要抛出。
注意:客户端的并发检查是牺牲的,服务器的工作流程是关键任务。
问题
我需要(从客户端进程)检查父实体是否已更改而不更新父实体的行版本。
在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
是交易破坏者,因为它会强制更改行版本。或者,在上下文中说,来自客户端进程的此检查将导致 父实体 中的行版本更改,这会不必要地干扰其他 客户端进程的 工作流。 p>
解决方法:我可能会将来自客户端进程的 SaveChanges
包装在 Transaction 中,然后随后读取父实体的行版本,反过来,如果行版本发生变化,则回滚。
总结
Entity Framework 是否有一种开箱即用的方式,我可以在其中SaveChanges
(在客户端进程中为子实体)还检查父实体的行版本是否已更改(不更新父实体的行版本)。
【问题讨论】:
是否可以使用 sqlserver rowversion 功能? docs.microsoft.com/en-us/sql/t-sql/data-types/… @ilkerkaran 是的,我正在使用它,但它更多的是如何检查父表上的并发更改而不更改该表上的rowversion
,因此SaveChanges
失败实体框架。
这个问题和你的类似吗? social.msdn.microsoft.com/Forums/en-US/….
修改父子的方法有很多,还是只有几个?换句话说,您的事务变通办法会导致重复代码吗?
我认为您的交易解决方法还不错。特别是。封装后,优点是它都在一个地方,所以很清楚会发生什么,并且不太可能有任何副作用。任何其他解决方案,例如使用 EF 的命令树拦截器,都将由分离的代码部分组成,从而很容易在频谱的任一侧破坏某些东西。 F.e.如果没有从数据库中提取父 rowid,这一切都会失败,当代码没有显示为什么/在哪里需要它们时,这很容易忘记。此外,也许更重要的是,它将您与这个版本的 EF 联系在一起。
【参考方案1】:
有一个非常简单的解决方案,“out-of-2-boxes”,但它需要两个修改,我不确定您是否可以或愿意进行:
在包含ParentRowVersion
列的子表上创建一个可更新视图
将子实体映射到此视图
让我展示一下它是如何工作的。这一切都很简单。
数据库模型:
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
视图是可更新的,因为它符合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
类模型
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 不会有问题。
【讨论】:
这实际上是一个非常巧妙的解决方案和创造性的解决方案,也是一种用视图达到预期结果的新颖方法。今天在工作中玩弄它,谢谢! "out-of-2-boxes" - 哈哈,我喜欢这样!如果 EF 允许我们用代码(表达式类型列?)创建这样的“视图”会很好,但它没有,所以这似乎是“两个世界”的最佳组合:) @Ivan tnx!是的,EF 可以做更多的图形/聚合意识,尽管我认为 EF-core 是朝那个方向迈出的“一小步”。我认为您能够将这种表达式类型的专栏想法实现为拉取请求,从而在该领域实现“巨大飞跃”。只需要一点时间... 这本着我所要求的精神最好地回答了这个问题 很好的答案!只是好奇如何在使用 EF Code First 进行模型定义时创建视图。【参考方案2】:好吧,您需要做的是在写入子实体时检查父实体的并发令牌(时间戳)。唯一的挑战是父时间戳不在子实体中。
您没有明确说明,但我假设您使用的是 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 类来拦截 EF Core 2 来执行此操作。https://weblogs.asp.net/ricardoperes/interception-in-entity-framework-core 上有一篇文章,Can I configure an interceptor yet in EntityFramework Core? 有一篇讨论。显然 EF Core 3(我认为它会在 9 月/10 月推出)将包含一个类似于 EF pre-Core 中的拦截机制,请参阅https://github.com/aspnet/EntityFrameworkCore/issues/15066
希望这对您有用。
【讨论】:
嗯,首先这是 不是 EF Core,而是 EF6 - 我专门询问了 OP,他们添加了正确的标签,在回答时应该已经存在。其次,关于唯一的挑战——对我来说这是一个相当大的挑战。虽然总体上这个想法似乎是正确的,但我希望看到它以一种通用的方式实现,尤其是使用非常违反直觉的 EF6 元数据模型。 是的,这绝对是实体框架而不是核心,今天我会进一步研究这种方法,看看它是否适用。 我在上面的回答中引用的最后一个链接重申了问题,并命名了与解决核心前问题相关的类。【参考方案3】:从一个项目到另一个项目,我在广泛的平台(不仅是 .Net)上遇到了这个问题。 从架构的角度来看,我可以提出几个并非 EntityFramework 特有的决策。 (至于我#2更好)
OPTION 1 实现乐观锁定方法。一般来说,想法听起来像:“让我们更新客户端然后检查父级的状态”。您已经提到了“使用事务”的想法,但是乐观锁定可以减少保留 Parent 实体所需的时间。比如:
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 表来分析“全局锁”设计模式。您可以在 http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
阅读到有关此方法的信息【讨论】:
以上是关于在不更新行版本的情况下检查实体的并发性的主要内容,如果未能解决你的问题,请参考以下文章