在不更新行版本的情况下检查实体的并发性

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)

我有一堆客户机进程,它们从该父实体访问只读值,并主要更新子实体

约束

  1. 客户之间不应干涉彼此的工作(例如,更新子记录不应在父实体上引发并发异常)。

  2. 我有一个确实更新父实体服务器进程,在这种情况下,如果父实体已更改,则客户端进程需要抛出。

注意:客户端的并发检查是牺牲性的,服务器的工作流程是关键任务

问题

我需要检查(从客户端进程中父实体是否已更改,而没有更新父实体的行版本

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

Ger*_*old 5

有一个非常简单的解决方案,“开箱即用”,但是它需要进行两项修改,我不确定您是否可以或愿意进行:

  • 在包含列的子表上创建可更新的视图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不会有问题。