こんにちは、弱小エンジニアのいすいです。
今日はEntity Framework Coreで悲観的ロックをする方法を備忘録で残しておきます。
間違いがあったらご教授いただけると幸いです。
やりたいこと
やりたいことしては以下です。
【API仕様】APIの処理でDBのデータから条件Aを満たした場合、新しくデータを追加する
【問題】同時アクセスを受けたときに「条件A」を両方満たし、両方ともデータが追加されてしまう
【理想】同時アクセスを受けた時、2番目の方が「条件A」を満たしていない場合は2番目の方はデータが追加されない
【解決策】1番目のアクセスでSelectしてからInsertするまで、後続のDBアクセスをロックする
この解決策が悲観的ロックという方法になります。
↓参考URL(悲観的ロックとは)
https://qiita.com/NagaokaKenichi/items/73040df85b7bd4e9ecfc
DBアクセスのロックには、DBレベルでのロックとトランザクションレベル(コード側で制御)の2つがあります。
SQL Server(他のDBでも)にロックの概念(UPLOCKなど)が存在していますが、
insertを制御する機能は見当たらず、そもそもトランザクションを制御するのはDBレベルでは無理なのでは?という結論に至りました。
(DBレベルでのロックは更新ロックが限界だと思われる)
解決方法
トランザクションにはトランザクション分離レベルというものが存在し、
トランザクション中のDBへのアクセスを段階ごとに制御できるらしい。
↓参考URL(トランザクション分離レベル(対象SQL Server)とは)
https://qiita.com/song_ss/items/38e514b05e9dabae3bdb
上記によるとトランザクション分離レベルはデフォルトではREAD COMMITTEDになっているらしく、SERIALIZABLEにすれば、悲観的ロックができそう
それがわかったら、Entity Framework Coreでトランザクション分離レベルを設定しよう。
(SQL Serverでもトランザクション分離レベルは設定できそうだが、EF Coreの方が早そう)
↓参考URL(EF Coreでのトランザクション(Microsoft公式)について)
https://docs.microsoft.com/ja-jp/ef/core/saving/transactions
上記によると、EF CoreではTransactionScopeでトランザクション分離レベルを設定できるみたい
なので、悲観的ロックをしたい処理を下記で囲めば良い
using (var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
// 処理(select~insert)
scope.complete();
}
実際にinsert処理に該当するSaveChanges()にブレークポイントを置いて、同時アクセスしてみてください。
ちゃんと条件に一致してない2番目の方はエラーで返されると思います。
以上でEF Coreの悲観的ロックが達成できること祈ります。
Thanks for the good article, I hope you continue to work as well.