コードレビューで見つかった問題を、勉強のためにまとめたものです(修正済み)。
この記事でわかること
save(update_fields=...)を間違えるとデータが壊れる理由- 「エラーも出ずにデータが消える」タイプのバグの怖さ
- 正しい直し方と、今後同じミスをしないための教訓
修正前のコード
写真メタデータを 更新する サービス関数の中に、こんなコードがあった。
# 修正前
allowed = {"width", "height", "taken_at"}
for field in allowed:
if field in meta:
setattr(photo, field, meta[field])
# ここが問題: meta にないフィールドも update_fields に入ってしまう
photo.save(update_fields=list(allowed))この関数は、photo が新規に作られている経路や、別の処理で taken_at が None のまま渡されてくる経路があった。撮影日時の EXIF が無い写真は珍しくありません。DB に保存済みの写真 (id=42) へ、新規インスタンス経由で meta = {"width": 1920} だけ渡された場合がこうなります。
開発者の意図は width だけの更新だった。コードレビューで指摘されるまで気づけない類のバグ。
なぜ起きるのか — update_fields の落とし穴
ポイントは、update_fields に指定したフィールドは、save 時点でメモリ上にある値がそのまま DB に書き込まれる こと。メモリ上の値は、そのインスタンスをどう作ったかで中身が違います。
注意したいのは、「setattr していないフィールドだから DB の現在値が温存される」というわけではない ということ。
つまり「DB から読んできた
photoを更新する」ケースなら、setattrしなかったフィールドはロード時の値がそのまま書き戻されるだけで実害はあまりありません。問題が出るのは、新規にモデルを作って一度もsetattrしていないフィールド や、別経路でNoneに再代入されているフィールド がupdate_fieldsに含まれているとき。今回のバグはまさにそれでした。
なぜ危険か
- 撮影日時は写真アプリの根幹データ。これが NULL になると、時系列ソートや年月別のフィルタが壊れます
- 問題が静かに起きる点が厄介。エラーは出ず、保存は成功します。データが消えているだけ
- 一度 NULL 上書きされたデータは復元できません(バックアップがない限り)
大切な思い出写真の「いつ撮ったか」という情報が静かに消えていく、というのはとても困る。
対策
update_fields は「実際に setattr したフィールド」だけで動的に組み立てるべき、というのが学んだルール。
# 修正後
allowed = {"width", "height", "taken_at"}
updated_fields = []
for field in allowed:
if field in meta:
setattr(photo, field, meta[field])
updated_fields.append(field) # setattr したものだけ記録
if updated_fields:
photo.save(update_fields=updated_fields) # 実際に更新したフィールドのみ保存直し後のデータの流れはこうなります。
学びポイント
save(update_fields=...)は「指定したフィールドの メモリ状態を強制的に DB に流し込む」操作。許可リストと、実際に値をセットしたリストは別管理にします- 「エラーが出ないから大丈夫」は危険な思い込み。Django の
save()はデータを黙って壊すことがあります - 動的に
update_fieldsを組み立てるパターン(updated_fields = []→append→save)を定型として覚えておきます
おわりに
このバグも 「動いてはいるが、壊れている」 タイプだった。save() が成功してログにもエラーが残らないのに、データが少しずつ削れていきます。これは経験しないと気づけない類の問題で、コードレビューで指摘されなかったらしばらく気づかなかったと思う。
XSS のような「外から攻撃される系」のバグと違って、こっちは 自分のコードが自分のデータを壊している タイプ。攻撃者は要りません、自分一人で被害を出せます。同じくらい怖い。
この記事は 2026 年 4 月時点の自分の理解で書いています。Django の挙動や推奨パターンは時間とともに変わる可能性があるので、参考にする際は最新の公式ドキュメント(特に
Model.save()のupdate_fieldsの項目)も合わせて確認してください。