AAA*_*ddd 16 c# sql-server entity-framework database-concurrency entity-framework-6
我有一个父实体,需要进行并发检查(如下所示)
[Timestamp]
public byte[] RowVersion { get; set; }
Run Code Online (Sandbox Code Playgroud)
我有一堆客户机进程,它们从该父实体访问只读值,并主要更新其子实体。
约束
客户之间不应干涉彼此的工作(例如,更新子记录不应在父实体上引发并发异常)。
我有一个确实更新此父实体的服务器进程,在这种情况下,如果父实体已更改,则客户端进程需要抛出。
注意:客户端的并发检查是牺牲性的,服务器的工作流程是关键任务。
问题
我需要检查(从客户端进程中)父实体是否已更改,而没有更新父实体的行版本。
在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;
Run Code Online (Sandbox Code Playgroud)
这IsModified = true
是交易突破者,因为它强制更改行版本。或者,在上下文中说,从客户端进程进行的检查将导致父实体中的行版本更改,从而不必要地干扰其他客户端进程的工作流。
解决方法:我可以将SaveChanges
来自客户端进程的包裹在Transaction中,然后依次读取父实体的行版本,如果行版本已更改,则依次回滚。
摘要
Entity Framework是否有一种现成的方式,我可以在其中(在子实体的客户端流程中)检查父实体的行版本是否已更改(而不更新父实体的行版本)。SaveChanges
有一个非常简单的解决方案,“开箱即用”,但是它需要进行两项修改,我不确定您是否可以或愿意进行:
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
Run Code Online (Sandbox Code Playgroud)
该视图是可更新的,因为它满足Sql Server视图可更新的条件。
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
Run Code Online (Sandbox Code Playgroud)
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; }
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
这段代码更新了一段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();
}
Run Code Online (Sandbox Code Playgroud)
现在SaveChanges
抛出所需的DbUpdateConcurrencyException
。注释掉父项的更新后,子项更新成功。
我认为这种方法的优点是它非常独立于数据访问库。您只需要一个支持开放式并发的ORM。将来转向EF-core不会有问题。