旧Entity FrameworkでEFデザイナーを用いてモデル生成した場合の競合検出方法をまとめる。
前提
ここではSqlServerを利用する。Saint
というDBにBronzeFighters
というテーブルがあるとする。
Name(主キー) | Zodiac | Master | RowVersion |
---|---|---|---|
氷河 | 白鳥座 | NULL | (自動生成) |
RowVersion
がタイムスタンプとして使うデータである。ここではrowversion
型として定義しているが、古い方のtimestamp
でも問題無い。他の列はnvarchar(N)
(10文字以上)であれば問題無い。ここで、RowVersion
の同時実行モード
をFixed
にしておく必要がある。
氷河
のデータを並行して次のように更新する。
-
Master
をカミュ
にする。 -
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}は、俺の師も同然の方です!」");
}
}
}
おじさん世代なら、テーブルの値を見て何がしたいかわかったかもしれない。要するに漫画とアニメで設定が大幅に違っているキャラであり、まさに競合のサンプルにふさわしいと言えよう。青田買いがひどすぎるよ!
実行結果は…
競合が検知できなかった。
対処法
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
に紐付けているのが面白い。
使い分け
例えば経理の経費精算承認処理のように、データの一部だけを更新するのなら前者の方法でFind()
なりSingleOrDefault()
して、必要なパラメータを更新、おまじないを加えればよいだろう。逆に、プロフィール更新画面などのテーブルのデータをほぼ全部入力するような使い方の場合は後者の方がスマートである。おまじない要素が無いところもポイントが高い。
今更CoreでないEntity Frameworkの、レガシー機能であるEF Designerを使う前提の情報がどれだけの人に役立つのかは分からないが、助けになれば幸いである。