Entity Framework EFデザイナー使用時の競合検知

旧Entity FrameworkでEFデザイナーを用いてモデル生成した場合の競合検出方法をまとめる。

## 前提

ここではSqlServerを利用する。`Saint`というDBに`BronzeFighters`というテーブルがあるとする。

| Name(主キー) | Zodiac | Master | RowVersion |
|:-:|:-:|:-:|:-:|
| 氷河 | 白鳥座 | NULL | (自動生成) |

`RowVersion`がタイムスタンプとして使うデータである。ここでは`rowversion`型として定義しているが、古い方の`timestamp`でも問題無い。他の列は`nvarchar(N)`（10文字以上）であれば問題無い。ここで、`RowVersion`の`同時実行モード`を`Fixed`にしておく必要がある。
![スクリーンショット 2023-04-14 174516.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666342/94121daf-312d-4cfa-014b-a4ab80513af9.png)

`氷河`のデータを並行して次のように更新する。

1. `Master`を`カミュ`にする。
1. `Master`を`クリスタル聖闘士`にする。

何も考えないでC#のコードを書くと、こんな感じになるだろうか。

``` C#
class Program
{
    static void Main()
    {
        Random rng = new Random();
        using (SaintEntities entities = new SaintEntities())
        {
            BronzeFighter comicVersion = entities.BronzeFighter.Find("氷河");
            BronzeFighter animeVersion = entities.BronzeFighter.Find("氷河");

            Task firstTask = UpdateMasterAsync(entities, comicVersion, "カミュ", rng);
            Task theOtherTask = UpdateMasterAsync(entities, animeVersion, "クリスタル聖闘士", rng);

            Task.WaitAll(firstTask, theOtherTask);

            BronzeFighter currentData = entities.BronzeFighter.Find("氷河");
            Console.WriteLine($"氷河の師匠は{currentData.Master}です。");
        }
    }

    static async Task UpdateMasterAsync(SaintEntities entities, BronzeFighter bronzeFighter, string masterName, Random random)
    {
        int wait = random.Next(1000, 5000);
        Console.WriteLine($"{wait}ミリ秒後に{bronzeFighter.Name}の師匠の名前を{masterName}にします。");

        await Task.Delay(wait);

        bronzeFighter.Master = masterName;
        try
        {
            await entities.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            Console.WriteLine($"{bronzeFighter.Name}「{masterName}は、俺の師も同然の方です！」");
        }
    }
}
```

おじさん世代なら、テーブルの値を見て何がしたいかわかったかもしれない。要するに漫画とアニメで設定が大幅に違っているキャラであり、まさに競合のサンプルにふさわしいと言えよう。青田買いがひどすぎるよ！

実行結果は…

![実行結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666342/9772ad41-a00b-3519-2053-e5f42a5f8ccb.png)

競合が検知できなかった。

## 対処法

### DBにデータが紐づいている場合

今回のようにDBから直接取得したデータを更新する場合は以下のようにする。[ネタ元はこちら](https://learn.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application)。

``` C#
static void Main()
{
    Random rng = new Random();
    using (SaintEntities entities = new SaintEntities())
    {
        BronzeFighter comicVersion = entities.BronzeFighter.Find("氷河");
        BronzeFighter animeVersion = entities.BronzeFighter.Find("氷河");
        byte[] rowversion = entities.BronzeFighter.Find("氷河").RowVersion;

        Task firstTask = UpdateMasterAsync(entities, comicVersion, "カミュ", rng, rowversion);
        Task theOtherTask = UpdateMasterAsync(entities, animeVersion, "クリスタル聖闘士", rng, rowversion);

        Task.WaitAll(firstTask, theOtherTask);

        BronzeFighter currentData = entities.BronzeFighters.Find("氷河");
        Console.WriteLine($"氷河の師匠は{currentData.Master}です。");
    }
}

static async Task UpdateMasterAsync(SaintEntities entities, BronzeFighter bronzeFighter, string masterName, Random random, byte[] rowversion)
{
    int wait = random.Next(1000, 5000);
    Console.WriteLine($"{wait}ミリ秒後に{bronzeFighter.Name}の師匠の名前を{masterName}にします。");

    await Task.Delay(wait);

    bronzeFighter.Master = masterName;
    try
    {
        entities.Entry(bronzeFighter).OriginalValues["RowVersion"] = rowversion;
        await entities.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        Console.WriteLine($"{bronzeFighter.Name}「{masterName}は、俺の師も同然の方です！」");
    }
}
```

肝は`RowVersion`を別口で取得することと、次のおまじないを`entities.SaveChanges()`前に挿入することだ。

``` C#
entities.Entry(bronzeFighter).OriginalValues["RowVersion"] = rowversion;
```
これにより`RowVersion`をチェックするという意図がEntity Frameworkに伝わり、無事クエリにも出力される（らしい）。

### DBにデータが紐づいていない場合

今回はコードがかなり変わる。

``` C#
static void Main()
{
    Random rng = new Random();
    Task firstTask = UpdateMasterAsync("カミュ", rng);
    Task theOtherTask = UpdateMasterAsync("クリスタル聖闘士", rng);

    Task.WaitAll(firstTask, theOtherTask);

    using (var entities = new SaintEntities())
    {
        BronzeFighter currentData = entities.BronzeFighter.Find("氷河");
        Console.WriteLine($"氷河の師匠は{currentData.Master}です。");
    }
}

static async Task UpdateMasterAsync(string masterName, Random random)
{
    using (SaintEntities entities =  new SaintEntities())
    {
        BronzeFighter bronzeFighter = entities.BronzeFighter.AsNoTracking().Single(fighter => fighter.Name == "氷河");
        int wait = random.Next(1000, 5000);
        Console.WriteLine($"{wait}ミリ秒後に{bronzeFighter.Name}の師匠の名前を{masterName}にします。");

        await Task.Delay(wait);

        bronzeFighter.Master = masterName;
        try
        {
            entities.Entry(bronzeFighter).State = System.Data.Entity.EntityState.Modified;
            await entities.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            Console.WriteLine($"{bronzeFighter.Name}「{masterName}は、俺の師も同然の方です！」");
        }
    }
}
```

更新タスクごとに接続を確保する必要があり、`UpdateMasterAsync()`内で`using`を使うことにした。データ取得もDBから切断されていることがわかりやすいようにFindを使わない形に改めた。`UpdateMasterAsync()`の引数も少なくなっている。

データの方をしれっと`entities`に紐付けているのが面白い。

実行結果は以下の通り。
![修正後実行結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666342/d6d4b715-b8da-f407-5197-02c7dcbbf3bd.png)

### 使い分け

例えば経理の経費精算承認処理のように、データの一部だけを更新するのなら前者の方法で`Find()`なり`SingleOrDefault()`して、必要なパラメータを更新、おまじないを加えればよいだろう。逆に、プロフィール更新画面などのテーブルのデータをほぼ全部入力するような使い方の場合は後者の方がスマートである。おまじない要素が無いところもポイントが高い。

今更CoreでないEntity Frameworkの、レガシー機能であるEF Designerを使う前提の情報がどれだけの人に役立つのかは分からないが、助けになれば幸いである。