web-dev-qa-db-ja.com

行バージョンを更新せずにエンティティの同時実行性を確認する

親エンティティ同時実行チェックを実行する必要があります(以下のように注釈を付けます)

[Timestamp]
public byte[] RowVersion { get; set; }

私はたくさんのクライアントプロセスにアクセスしますreadonlyこれからの値親エンティティそして主にpdateその- 子エンティティ

制約

  1. Clientsは互いの作業を妨げるべきではありません(たとえば、子レコードの更新はparent entityで同時実行例外をスローするべきではありません)。

  2. 私はserver process that doespdate this parent entity、そしてこの場合はclientプロセス親エンティティが変更されている場合、スローする必要があります。

:クライアントの同時実行性チェックは犠牲になり、サーバーのワークフローはミッションクリティカルです

問題

クライアントプロセスから親エンティティが変更されたかどうかを確認する必要があります親エンティティの行バージョンを更新せずに

[〜#〜] 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 = truedeal breakerです。行のバージョンを強制的に変更するためです。または、コンテキストで言うと、クライアントプロセスからのこのチェックは親エンティティで行バージョンの変更を引き起こし、他のクライアントプロセス 'ワークフローに不必要に干渉します。

回避策:クライアントプロセスからのSaveChangesをトランザクションにラップし、その後、親エンティティの行を読み取る可能性がありますバージョン、順番に、行のバージョンが変更された場合はロールバックします。

概要

out-of-the-boxEntity Frameworkを使用する方法はありますか。ここでSaveChangesclient process for 子エンティティ)さらに親エンティティの行バージョンが変更されたかどうかも確認します(親エンティティ行バージョンを更新せずに)= =)。

17
Michael Randall

「out-of-2-boxes」という驚くほど簡単な解決策がありますが、2つの変更が必要かどうかはわかりません。

  • ParentRowVersion列を含む子テーブルにupdatableビューを作成します
  • 子エンティティをこのビューにマップします

これがどのように機能するかを示しましょう。それはすべて非常に簡単です。

データベースモデル:

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

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

クラスモデル

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への将来の移行は問題になりません。

6
Gert Arnold

まあ、あなたがする必要があるのは、子エンティティに書き込むときに親エンティティの同時実行トークン(タイムスタンプ)を確認することです。唯一の課題は、親タイムスタンプが子エンティティにないことです。

あなたは明示的に述べていませんでしたが、私はあなたがEF Coreを使用していると想定しています。

https://docs.Microsoft.com/en-us/ef/core/saving/concurrency を見ると、EF CoreがUPDATEまたはDELETEがゼロ行に影響する場合の並行性例外。同時実行性テストを実装するために、EFは、同時実行性トークンをテストするWHERE句を追加し、UPDATEまたはDELETEによって正しい行数が影響を受けたかどうかをテストします。

親のRowVersionの値をテストする追加のWHERE句をUPDATEまたはDELETEに追加することができます。 System.Diagnostics.DiagnosticListenerクラスを使用してこれを実行し、EF Core 2をインターセプトできると思います。 に記事があります(== --- ==)https://weblogs.asp.net/ ricardoperes/interception-in-entity-framework-core および でのディスカッションEntityFramework Coreでインターセプターをまだ構成できますか? 。明らかにEF Core 3(9月または10月にリリースされると思います)には、EF pre-Coreと同様のインターセプトメカニズムが含まれます。 を参照してくださいhttps://github.com/aspnet/EntityFrameworkCore/issues/15066

これがあなたに役立つことを願っています。

3
sjb-sjb

プロジェクトごとに、(。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 pls方法を参照してください。 EF CoreでSelect For Updateを実装する方法

オプション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.SaveChanges() != 1 )
    throw new Exception();

また、追加のDBテーブルを使用して、「グローバルロック」設計パターンを分析してみてください。あなたはそこでこのアプローチについて読むことができます http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

2
Dewfy