YASD-TECH
YASD TECH
# Rails

dependent オプションの使い分け(destroy・nullify・restrict)

投稿日:2026/8/5

更新日:2026/8/5

ttitleImage

dependent オプションの使い分け(destroy・nullify・restrict)

has_many に付ける dependent: は、親レコードが削除されたとき子レコードをどうするかを決める Rails の関連オプション。
「カテゴリを消したら記事まで消えた」という事故はここの選択ミスで起きる。選び方と、:nullify を選んだときに必ず踏むハマりどころをまとめる。

結論

  • 子が独立した価値を持つデータなら :nullify(子は残し、外部キーだけ NULL にする)
  • 子が親に完全従属するなら :destroy(親と一緒に消す)
  • :nullify を使うなら、外部キーカラムは null: true で定義されていなければならない。この2つはセットで設計する
  • :nullify が戻すのは外部キーカラムだけ。「カテゴリ内での表示順」のような親に紐づく他のカラムは取り残されるので、自前で戻す

題材

ブログ(Blog)が記事カテゴリ(ArticleCategory)を持ち、カテゴリに記事(Article)が並ぶ、という3層で考える。

erDiagram
    blogs ||--o{ article_categories : "1ブログに複数カテゴリ"
    article_categories |o--o{ articles : "1カテゴリに複数記事(未分類あり)"
    blogs {
        int id
        string title
    }
    article_categories {
        int id
        int blog_id
        string name
        int priority
    }
    articles {
        int id
        int article_category_id "NULL 可"
        string title
        int category_priority
    }
ruby
class Blog < ApplicationRecord
  has_many :article_categories, -> { order(priority: :asc) }, dependent: :destroy
end

class ArticleCategory < ApplicationRecord
  belongs_to :blog
  has_many :articles, dependent: :nullify
end

class Article < ApplicationRecord
  belongs_to :article_category, optional: true
end

カラム定義側はこうなっている。:nullify が成立する前提はここ。

ruby
create_table :articles do |t|
  t.references :article_category, null: true, foreign_key: true
  t.string :title, null: false
  t.integer :category_priority, null: false, default: 0 # カテゴリ内での表示順
end

articles.article_category_idnull: true だから :nullify が使える。
null: false なら :nullify は実行時に落ちる(後述)。


発行される SQL

ArticleCategory#destroy を呼ぶと、:nullify1本の UPDATE を撃ってから親を消す。

sql
UPDATE articles SET article_category_id = NULL
WHERE article_category_id = '削除するカテゴリのid';

DELETE FROM article_categories WHERE id = '削除するカテゴリのid';

:destroy だったら、子を1件ずつ destroy する(= 子のコールバックが走り、さらにその孫にも dependent が連鎖する)。

sql
DELETE FROM articles WHERE id = '子1';
DELETE FROM articles WHERE id = '子2';
DELETE FROM article_categories WHERE id = '...';

選択肢の比較

指定 親を消したときの子の扱い 子のコールバック 発行クエリ
:destroy 子も1件ずつ削除 走る 子の件数ぶん DELETE
:delete_all 子をまとめて削除 走らない DELETE 1本
:nullify 子は残し、外部キーだけ NULL 走らない UPDATE 1本
:restrict_with_error 子がいると親を削除させない(バリデーションエラー) 存在チェックの SELECT
:restrict_with_exception 子がいると ActiveRecord::DeleteRestrictionError 存在チェックの SELECT
:destroy_async 子の削除をジョブに逃がす(Rails 6.1+) 走る(非同期) ジョブ登録
指定なし 何もしない 親の DELETE のみ

指定なしは「安全」ではない。 DB 側に外部キー制約があれば ActiveRecord::InvalidForeignKey で落ち、制約が無ければ存在しない親を指す孤児レコードが静かに残る。どちらも事故なので、has_many を書いたら dependent: は必ず意識して決める。


どう選ぶか — 「子は親の持ち物か?」

判断軸は1つだけ。子は親に従属しているのか、それとも子自身が業務の中核データなのか。

flowchart TD
    Q1{"親が消えたとき<br/>子も消えて意味が通るか?"}
    Q1 -->|はい・子は親の一部| Q2{"子にコールバックや<br/>孫の dependent があるか?"}
    Q1 -->|いいえ・子は独立データ| Q3{"親を消した後も<br/>子を残してよいか?"}
    Q2 -->|ある| D1["dependent: :destroy"]
    Q2 -->|ない・大量件数| D2["dependent: :delete_all"]
    Q3 -->|残してよい| D3["dependent: :nullify"]
    Q3 -->|子がいる間は消させない| D4["dependent: :restrict_with_error"]

この題材に当てはめるとこうなる。

関連 選択 理由
BlogArticleCategory :destroy カテゴリはブログに完全従属する。ブログが消えたらカテゴリだけ残っても意味がない
ArticleCategoryArticle :nullify 記事は中核データ。カテゴリは「並べ方」の付加情報にすぎない

カテゴリはあくまで陳列棚であって、記事の所有者ではない。運営者が「イチオシ」というカテゴリを消したときに、そこに入れていた記事まで消えたら事故になる(記事に紐づくコメントや公開履歴まで巻き添えになる)。

一方 :restrict_with_error にすると「記事が入っているカテゴリは消せません」となり、棚を整理するだけの UI としては使いづらい。

同じアプリの中でも、関連の性質ごとに使い分ける。「このモデルは :destroy で統一」のようなモデル単位の決め方はしない。


ハマりどころ

1. null: false:nullify は同時に成立しない

:nullify は外部キーに NULL を入れる。カラムが null: false なら当然落ちる。

ActiveRecord::NotNullViolation: null value in column "article_category_id" violates not-null constraint

しかも親を消すまで発覚しないdependent: :nullify を書いたら、対になるマイグレーションが null: true になっているかを必ず一緒に確認する。

2. :nullify は外部キー以外のカラムを戻さない

ここが一番踏みやすい。:nullify が触るのは外部キー(とポリモーフィックの type)だけで、他のカラムはそのまま残る。

今回の題材では、記事に category_priority(カテゴリ内での表示順)という別カラムがある。:nullify 任せにすると、カテゴリを消された記事は

所属なし(article_category_id は NULL)・でも表示順は 3 のまま

という中途半端な状態になる。未分類の一覧を category_priority で並べると、消えたカテゴリ時代の古い順序が割り込んでくる。

そのため Form 側で、削除対象カテゴリに属していた記事の ID を消す前に拾っておき、後で表示順も戻す。

ruby
# app/forms/article_category_arrangement_form.rb
removed_categories = existing_categories.values_at(*(existing_categories.keys - input_ids))

# dependent: :nullify は所属を外すだけで表示順を残すため、削除対象の所属記事は後で表示順も戻す
@orphaned_article_ids = removed_categories.flat_map { |category| category.articles.ids }

removed_categories.each(&:destroy!)
ruby
# 後段:拾っておいた ID の表示順を未設定(0)に戻す
Article.where(id: @orphaned_article_ids).update_all(category_priority: 0)

destroy! より前に ID を確定させること。 消した後では category.articles は空になっていて、対象を特定できない。
レビューでは「ID を拾う処理」と「表示順を戻す処理」が対になっているかを見る。

3. :nullify / :delete_all では子のコールバックが走らない

:nullify は UPDATE 1本、:delete_all は DELETE 1本で済ませるので、子の before_destroyafter_commit は実行されない。子側でキャッシュ更新・検索インデックス同期・通知などをコールバックに載せている場合、それらは黙ってスキップされる

副作用を確実に走らせたいなら :destroy(件数が多いなら :destroy_async)を選ぶか、親側で明示的に呼ぶ。

関連: モデルとコールバッククラスの分離パターン

4. belongs_to 側の dependent: は意味が違う

belongs_to にも dependent: は書けるが、指定できるのは :destroy / :delete だけで、「子を消したら親も消える」という逆方向の挙動になる。has_many のつもりで書くと親が巻き込まれるので注意。


まとめ

  • dependent: は「親が消えたときの子の後始末」の宣言。書き忘れは孤児レコードか外部キー違反になる
  • 子が独立データなら :nullify、完全従属なら :destroy、消させたくないなら :restrict_with_error
  • :nullifynull: true とセット、そして 外部キー以外のカラムは自分で戻す

参考

Index

  • dependent オプションの使い分け(destroy・nullify・restrict)
  • 結論
  • 題材
  • 発行される SQL
  • 選択肢の比較
  • どう選ぶか — 「子は親の持ち物か?」
  • ハマりどころ
  • 1. null: false と :nullify は同時に成立しない
  • 2. :nullify は外部キー以外のカラムを戻さない
  • 3. :nullify / :delete_all では子のコールバックが走らない
  • 4. belongs_to 側の dependent: は意味が違う
  • まとめ
  • 参考