おーみんブログ

C#, ASP.NET Core, Unityが大好きです。

EntityFramework Core + SQLiteでオプティミスティック同時実行制御を行う方法。

はじめに

EntityFramework CoreとSQLServerでオプティミスティック同時実行制御を実装する際はSQLServerのROWVERSION列を使うのが一般的ですが、SQLServer以外でオプティミスティック同時実行制御を実装する際は少し工夫が必要です。

サンプルコード

以下のMS Docsを参考に今回は実装を行いました。
パート 8、ASP.NET Core の Razor ページと EF Core - コンカレンシー | Microsoft Docs

[DbContext.cs]

public SampleMVCAppContext (DbContextOptions<SampleMVCAppContext> options)
    : base(options)
{
}

public DbSet<Person> Person { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property(d => d.ConcurrencyToken)
        .IsConcurrencyToken();
}

こちらについてはConcurrencyTokenプロパティをConcurrencyTokenとして取り扱うよう設定しています。 (名前が同じでちょっと分かりにくかったかもですね...。)

以下にMS Docsからの引用文も記載させていただきます。

IsConcurrencyToken は、プロパティをコンカレンシー トークンとして構成します。 更新時に、データベース内のコンカレンシー トークンの値が元の値と比較され、データベースからインスタンスが取得されてから変更されていないことが確認されます。 変更されている場合、DbUpdateConcurrencyException がスローされ、変更は適用されません。

DbContextの設定後、EFCoreのマイグレーション機能を利用してデータベースを更新します。

Add-Migration
Update-Database

[Person.cs]

public class Person
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string Mail { get; set; }
    public int Age { get; set; }

    public Guid ConcurrencyToken { get; set; } = Guid.NewGuid();
}

[PepleController]

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("PersonId,Name,Mail,Age,ConcurrencyToken")] Person person)
{
    if (id != person.PersonId)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        var target = _context.Person.Where(o => o.PersonId == person.PersonId).FirstOrDefault();
        // 更新内容
        target.Name = person.Name;
        target.Mail = person.Mail;
        target.Age = person.Age;
        // 新しくGuidを生成
        target.ConcurrencyToken = Guid.NewGuid();
        _context.Entry(target)
           .Property(d => d.ConcurrencyToken).OriginalValue = person.ConcurrencyToken;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!PersonExists(person.PersonId))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(person);
}

同時実行してみる

対象のデータを更新する際に別タブでそれぞれ開き、各々更新してみます。 片方は正常に値が更新されることが確認されましたが、もう片方はDbUpdateConcurrencyExceptionエラーが出ていることが分かります。

[正常に更新された方] f:id:bookreadkun:20210905221406p:plain

[同時実行制御にかかった方] f:id:bookreadkun:20210905221459p:plain

おわりに

最初通常のSQLServerの方のやり方でやっていたのですが、全くできずにかなりハマってしまいました( ;∀;) データベースによってもやり方が異なるのでしっかり理解していかないとですね...。