やりたかったこと
IDが重複しないことを確認してから、DBにレコードを追加したかった。(IDの自動インクリメントは使わないものとする)
ユニーク制約があるので、重複登録しようとしてもエラーになるのだが、
予めチェックしてエラーにならないようにしたかった。
トランザクション分離レベルをSERIALIZABLEにすれば可能だと思っていた
SERIALIZABLEはwikipediaによると
複数の並行に動作するトランザクションそれぞれの結果が、いかなる場合でも、それらのトランザクションを時間的重なりなく逐次実行した場合と同じ結果となる。
とのこと。 「時間的重なりなく逐次実行した場合と同じ結果となる」のだから だから特に問題なく目的が達成できると思っていた。
だが出来なかった。
以下実行イメージ
お題:【TableAにID=100が存在しないことを確認してからID=100のレコードをTableAに追加する】 postgreSQL 10.4
TransactionAにてTableAにID=100が存在しないことを確認
// TransactionA postgres=# START TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION postgres=# SELECT COUNT(*) FROM todo_lists_item WHERE id = 100; count ------- 0 (1 row) //存在しないことが確認できた
ここでTransactionBでも同じ処理が走り出したと仮定
// TransactionB postgres=# START TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION postgres=# SELECT COUNT(*) FROM todo_lists_item WHERE id = 100; //ここで処理がとまると思っていた count ------- 0 (1 row) //SELECT 文が実行されてしまった。(もちろんid=100は存在しない)
SERIALIZABLEのイメージだとTransactionAが完了するまでTransactionBのいかなるクエリも待たされるのかと思っていた。 実際には上記のようにTransactionAが動作中にもかかわらず、TransactionBのSelect文が実行されて結果が帰ってきてしまった。
この後はどうなっていくのか?
TransactionAの処理が進む
// TransactionA postgres=# INSERT INTO TableA VALUE(100, 'TransactionAから追加') INSERT 0 1 //追加が実行された
TransactionBも処理がすすむ。(現在TransactionAでid=100が追加された直後)
// TransactionB postgres=# INSERT INTO TableA VALUE(100, 'TransactionBから追加') // ここで待ちが発生する
ここで結果が帰ってこなくなり、TransactionBに待ちが発生した。
TransactionAの処理が完了する
// TransactionA postgres=# COMMIT; COMMIT
するとTransactionBの待ちが解除されて処理が進んだが、エラーになってしまった。
// TransactionB ERROR: could not serialize access due to read/write dependencies among transactions DETAIL: Reason code: Canceled on identification as a pivot, during write. HINT: The transaction might succeed if retried.
ID重複がないのを確認してからレコード追加は結局どうすればいいのか?
トランザクション内で
重複無いことを確認=>登録
とやっても、上記ようにエラー発生の可能性があるならプログラム側で例外キャッチして判断するのが正解?
Serializableといえども SELECTまでロックしてしまうと、処理がかなり遅くなりそうなんでこういう仕様になっているということだろうか?
(追記) 結論でた。=> トランザクション内で「重複チェック」してから「DB登録実行」しても重複が発生する場合がある - やる気がストロングZERO