0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-04-17

旧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

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

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

何も考えないで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

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

対処法

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

今回のようにDBから直接取得したデータを更新する場合は以下のようにする。ネタ元はこちら

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()前に挿入することだ。

entities.Entry(bronzeFighter).OriginalValues["RowVersion"] = rowversion;

これによりRowVersionをチェックするという意図がEntity Frameworkに伝わり、無事クエリにも出力される(らしい)。

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

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

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

使い分け

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?