コードレビューで見つかった問題を、勉強のためにまとめたものです(修正済み)。
この記事でわかること
- Django で
Model.objects.create()を for ループで回すと何が起きるか transaction.atomic()が内部で何をして「全部か無か」を保証するのか(SAVEPOINT の仕組み)transaction.atomic()とbulk_createを組み合わせて整合性を守る方法- 所有者チェックの「ループ内
get_object_or_404」が壊れやすい理由
概念
Django の ORM は「1 クエリ = 1 トランザクション」がデフォルトで、Model.objects.create() を for ループで回すと 1件ごとに独立したトランザクションが張られて commit されます。
例えると、レジで3点まとめて会計している途中で、2点目でカードが弾かれたのに1点目だけ会計が済んで手元に残る——みたいな状態です。本来「3点まとめて1つの会計」のはずが、半端な結果だけ残ってしまいます。
これを防ぐには with transaction.atomic(): ブロックで全体を囲み、全部成功するか、全部ロールバックするか のどちらかに寄せる必要があります。
修正前のコード
アルバム作成時、「アルバム本体を作る」→「渡された写真 ID 群から中間テーブルを作る」を for ループで処理していた。所有者チェックがループ内にあり、他人の ID が紛れ込んだ時点で 404 を返して処理が止まります。
# 修正前
album = Album.objects.create(user=request.user, ...) # ← ここで commit
for order, photo_id in enumerate(photo_ids):
photo = get_object_or_404(Photo, pk=photo_id, owner=request.user)
# ↑ 他人の photo_id で 404 発生 → ここまでに作った中間レコードは残る
AlbumPhoto.objects.create(album=album, photo=photo, display_order=order)なぜ危険か
- データが取り戻せない: 空のアルバムや一部写真だけのアルバムが残り、ユーザーは「作った気」だが UI には崩れたデータが出ます
- リトライで二重作成: エラー画面でユーザーが再実行すると、壊れたアルバムの隣にもう1つアルバムが作られて増殖します
- マイグレーションの爆弾: 将来「アルバムには必ず1枚以上の写真が付く」という制約を足そうとしたとき、既存の壊れたデータで通りません
対策
全体を transaction.atomic() で囲み、Photo の取得は一括で、中間レコードは bulk_create() で 1 クエリに。
from django.db import transaction
from rest_framework.exceptions import ValidationError
with transaction.atomic(): # ← ここから1つの塊。中で失敗したら全部巻き戻る
album = Album.objects.create(user=request.user, ...) # 本体を作る
# 所有者チェックを「一括」で。ループ内 get_object_or_404 をやめ、
# 自分が持っている写真だけを1クエリで取得する
photos = Photo.objects.filter(pk__in=photo_ids, owner=request.user)
if photos.count() != len(photo_ids):
# 他人の ID が混ざってた → 例外で中断。atomic 内なので album 作成ごと巻き戻る
raise ValidationError("所有していない写真が含まれています")
photo_map = {p.pk: p for p in photos}
AlbumPhoto.objects.bulk_create([ # 中間レコードを1クエリでまとめて作成
AlbumPhoto(album=album, photo=photo_map[pid], display_order=i)
for i, pid in enumerate(photo_ids)
])仕組み(atomic は内部で何をしているか)
with transaction.atomic(): の中で Django がやってくれているのは、ざっくりこの4ステップ。
- ブロック開始時に SAVEPOINT(巻き戻し用のしおり)を発行します
- ブロック内の SQL を発行します(
AlbumINSERT →AlbumPhotoINSERT...) - 最後まで正常終了したら RELEASE(コミット相当)で確定させます
- 途中で例外が出たら ROLLBACK TO SAVEPOINT で、ブロック開始時点まで巻き戻します
なお atomic() は 入れ子(ネスト)にもできます。外側の atomic がまだコミット前なら、内側で例外が出て巻き戻っても外側は生きています。複雑な処理の一部だけをロールバックしたいときに効きます。
学びポイント
- 「複数レコードを作る/削除する」処理は atomic で囲います。これは機械的ルールでいいです
- 所有者チェックをループ内で
get_object_or_404するのは「書きやすいが壊れやすい」典型パターンです。filter(owner=...)で 一度に検証 する方がバグも速度も良いです bulk_create/bulk_updateは「速い」だけでなく「整合性が揃いやすい」。個人データを扱うサービスほど意識したい
おわりに
「ハッピーパスだけ書く」と起きる典型バグで、本人が気づきにくいタイプの不整合。atomic() は 書いた人が偉い タイプの安全装置で、コードレビューでも「複数 create / update があるのに atomic で囲まれてない」は機械的に指摘できます。
レビューチェックリストに「複数レコード書き込みは atomic で囲ってるか」を入れておくと、AI 生成コードの取りこぼしも拾いやすいです。
なお「毎回 atomic で囲むべきか/囲まなくていい場面はあるか」という使いどころの判断は、別記事 2ステップ保存で空ページが残った — transaction.atomic() を「いつ使い、いつ使わないか」 で整理しています。
この記事は2026年4月時点の自分の理解で書いています。Django の
transactionは安定した領域ですが、最新の公式ドキュメントも合わせて参照してください。