見出し画像

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)


ここまでお読みいただきありがとうございました!


参考


この記事が気に入ったらサポートをしてみませんか?