Django_transaction.atomicでハマった話 #368
結局自力では解決できなかったのですが、エラー事例の共有としてメモしておきます。
Djangoでモデル操作する際のデフォルトはオートコミットです。つまりcreateやdeleteなどを行うとその都度コミットされます。
ただ、一連の処理を以下のようにtransactionでラップできます。
with transaction.atomic(using=DBの名前):
SampleModel.objects.bulk_update(updated_objects, ['column1', 'column2'])
SampleModel.objects.bulk_create(new_objects)
こうすることでbulk_updateとbulk_createを一連の処理としてまとめ、処理の途中でエラーが起これば全ロールバックされるはずです。
しかし、上記のようにラップしてもtransactionが効かず、bulk_updateとbulk_createそれぞれでコミットされてしまうエラーにハマっていました。
他にもsave()やdelete()、create_or_update()などでも試しましたが同じでした。一方でDjangoを介さずデータベースを直接操作する検証ではtransactionが効いたので、データベース側の問題ではありません。
結論
「using=DBの名前」で別DBを指定してしまっていた。
原因
我々のソースコードではtransaction.atomicを以下のように実装するのがデフォルトになっていました。「using」に注目してください。
with transaction.atomic(using=DataBaseUtil.database_name()):
SampleModel.objects.bulk_update(updated_objects, ['column1', 'column2'])
SampleModel.objects.bulk_create(new_objects)
DataBaseUtil.database_name()はオリジナル関数で、デフォルトのDB名を返します。ここで返されているDB名が想定と違った、というのがオチです。
別DBに対してtransactionをかけていたので、対象DBではDjangoのオートコミットがそのまま効いてしまっていました。
結論だけ見るとシンプルですが、ここに行き着くのに非常に多くの人のご知見をお借りし、何パターンも検証し、最終的にはマネージャーが発見してくださいました。
もし同様にtransactionが効かなくて困っている人がいらっしゃれば、対象のDBをきちんと指定できているか確認してみてください。
その他の補足
atomic()にはdurableという引数があり、これにTrueを渡すことで「最も外側のtransactionである」ことを保証できます。
with transaction.atomic(using=DBの名前, durable=True):
SampleModel.objects.bulk_update(updated_objects, ['column1', 'column2'])
SampleModel.objects.bulk_create(new_objects)
「durable=True」となっているtransactionが最も外側ではない場合、RuntimeErrorが出ます。極端な例ですが、例えば以下です。
with transaction.atomic(using=DBの名前):
with transaction.atomic(using=DBの名前, durable=True):
SampleModel.objects.bulk_update(updated_objects, ['column1', 'column2'])
SampleModel.objects.bulk_create(new_objects)
ここまでお読みいただきありがとうございました!