コードレビューで見つかった「空ページが残る可能性がある」指摘を、修正と一緒にまとめたものです(修正済み)。
この記事でわかること
- DB に 2 つ以上の書き込みをするとき「片方だけ成功する」とどうなるか
- Django の
transaction.atomic()がやってくれること - ビューの create で transaction を使うべき場面と、使う必要がない場面の見分け方
起きていたコード
テンプレートページ(写真とコメントの組み合わせ)を作成する DRF ビュー。元のコードはこんな構造だった:
def create(self, request, *args, **kwargs):
ser = TemplatePageCreateSerializer(data=request.data)
ser.is_valid(raise_exception=True)
# 1. ページ本体を作る
page = TemplatePage.objects.create(
owner=request.user,
title=ser.validated_data["title"],
template_type=ser.validated_data["template_type"],
)
# 2. ページに紐づく entries を bulk_create する
TemplatePageEntry.objects.bulk_create([
TemplatePageEntry(
page=page,
meaning=meanings[e["meaning_id"]],
...
)
for e in entries_data
])
return Response(TemplatePageDetailSerializer(page, ...).data, ...)普通に動く。普通に動くんだけど、レビューで指摘されたのは:
bulk_create が途中で失敗すると、page だけ作られて entries 0 件のレコードが DB に残る
なぜ「空ページ残留」が問題か
- ユーザは失敗と見て再度作り直します
- 後で一覧を表示すると、空ページが混ざる → 操作不能 or 不思議な UI 挙動
- 再現性のあるバグの温床になります
セキュリティ問題ではないが、データ整合性の問題。
なぜ bulk_create が失敗しうるのか
- 接続切断(DB との TCP が切れる)
- ロックタイムアウト(他のトランザクションと競合)
- 制約違反(外部キーが指す先が削除された 等)
- メモリ不足・タイムアウト(巨大な bulk)
「100% 落ちる」とは限らないが、運用していれば必ず数回は遭遇するライン。
直し方: transaction.atomic()
Django の transaction.atomic() を使うと、ブロック内の DB 書き込みが全部成功するか / 全部ロールバックされるかの二択になります:
from django.db import transaction
def create(self, request, *args, **kwargs):
ser = TemplatePageCreateSerializer(data=request.data)
ser.is_valid(raise_exception=True)
with transaction.atomic(): # ← ここから下を「全部成功 or 全部なし」の1かたまりにする
page = TemplatePage.objects.create(...)
TemplatePageEntry.objects.bulk_create([...]) # 途中でコケたら page 作成ごと巻き戻る
return Response(...)仕組み(ざっくり)
with transaction.atomic(): は内部で「ブロック開始時に SAVEPOINT を張り、最後まで通れば確定(コミット相当)、途中で例外が出れば開始時点まで巻き戻す(ROLLBACK)」という動きをします。
SAVEPOINT のステップごとの中身やネストの挙動は、原典の transaction.atomic() の「全部か無か」はどう効くのか で詳しく書いた。この記事では「で、結局いつ使うのか」に絞ります。
いつ使う?
毎回 transaction.atomic() で囲めばいいのかというと、そうでもありません。コストもあります(SAVEPOINT 発行、競合発生時のロック保持時間が伸びる)。
使うべき場面の目安:
- 1リクエストで複数テーブルに書き込みを横断します(今回のケース)
- 中間状態に意味がありません(page 単独 / entries 単独で残しても意味がありません)
- 整合性が崩れると後続のユーザ操作が壊れます
使わなくていい場面:
- 単一テーブルの単一 INSERT/UPDATE
- 失敗してもユーザが再試行すれば良い独立した処理(例: ログ書き込み)
- 並列性を優先したい(ロック保持時間を短くしたい)
学びポイント
- 「1リクエスト内で書く先が複数ある」コードを見たら、原子性を考える癖をつけます
- Django なら
with transaction.atomic():で原子性は1行で確保できます - 「動いてるから OK」と「データ整合性 OK」は別。運用していれば必ず障害は起きる前提で書きます
bulk_createは「複数 INSERT を 1 ラウンドトリップで送る」最適化であって、原子性は保証しません(atomic で囲んで初めてセット)
おわりに
「正常系で動く」コードと「障害時にも壊れない」コードは別物で、後者を書くには 「これ落ちたらどうなる?」を全 step ごとに想像する癖が要ります。
transaction.atomic() はその想像に応える Django の道具。今回みたいに 2 ステップに分かれてる時こそ、囲むことで「中間状態は許さない」をコードで表現できます。
この記事は2026年5月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。