投稿日:2026/8/5
更新日:2026/8/5

inverse_of: は「この関連の裏返しは、相手モデルのこの関連だよ」と手で教える指定。
普段は Rails が名前から自動推測してくれるので書かなくていいが、推測が効かない条件があり、そこだけ明示が要る。
「スコープを書いたら inverse_of もセット」と覚えている人が多いが、Rails 7.0 でそのルールは変わっている。
inverse_of を書かないと、親子が別インスタンスになる(無駄な SELECT が飛ぶ・変更が同期しない・未保存の親に build した子が落ちる)。:through と :foreign_key を指定した関連は、バージョンを問わず自動推測されない。ここは常に明示。-> { ... })は Rails 6.1 以前は自動推測されない。Rails 7.0 以降は config.load_defaults 7.0 以上なら推測される。load_defaults が上がっていないことがあるので、迷ったら書く。書いても害はない。プロジェクトとタスクの1対多で見る。
class Project < ApplicationRecord
has_many :tasks,
-> { order(position: :asc) },
dependent: :destroy,
inverse_of: :project # ← Task 側の belongs_to :project を指している
end
class Task < ApplicationRecord
belongs_to :project
end
inverse_of: :project は Task の関連名を書く。DB の話は一切していない。メモリ上のオブジェクトを繋いでよいかを Rails に教えているだけ。
flowchart LR
subgraph mem["メモリ上"]
P["project<br/>(Project インスタンス)"]
T["task<br/>(Task インスタンス)"]
end
P -->|"project.tasks"| T
T -->|"task.project<br/>← これを P に繋ぐのが inverse_of"| P
project = Project.first
task = project.tasks.first
task.project
# inverse_of あり → メモリ上の project をそのまま返す(クエリ 0 回)
# inverse_of なし → SELECT * FROM projects WHERE id = ... LIMIT 1
一覧をループで回すと、そのままタスク件数ぶんの N+1 になる。
同じ行なのに別インスタンスになるので、片方への変更がもう片方に見えない。
project.name = "改名後"
task.project.name # あり → "改名後" / なし → "改名前"
task.project.equal?(project) # あり → true / なし → false
これは「保存したのに反映されない」系のバグとして出てくる。デバッグ時に最も気づきにくいのがこれ。
新規の親に build して一括保存するパターン。
project = Project.new(name: "新規プロジェクト")
task = project.tasks.build(title: "最初のタスク")
task.project # あり → project(未保存でも辿れる) / なし → nil
project.save # なし → Task の "Project must exist" で失敗
belongs_to は Rails 5 以降デフォルトで必須。親がまだ id を持たない時点で子のバリデーションが走るため、FK ではなくオブジェクト参照で親を見つけられないと落ちる。逆に inverse_of が効いていれば、関連経由で new / build した子は最初から親を持っている。
# フォームオブジェクトなどでの典型
record = project.tasks.new # ← この時点で record.project が入る
Rails は has_many :tasks から「相手は Task の project だろう」と、クラス名を単数スネークケースにした名前で探しに行く。当たれば自動で繋ぐ。
判定は ActiveRecord::Reflection の can_find_inverse_of_automatically? に集約されている。
def can_find_inverse_of_automatically?(reflection, inverse_reflection = false)
reflection.options[:inverse_of] != false &&
!reflection.options[:through] &&
!reflection.options[:foreign_key] &&
scope_allows_automatic_inverse_of?(reflection, inverse_reflection)
end
flowchart TD
Start["has_many / belongs_to を辿る"] --> A{"inverse_of: false ?"}
A -->|"yes"| NG["自動推測しない"]
A -->|"no"| B{":through がある ?"}
B -->|"yes"| NG
B -->|"no"| C{":foreign_key がある ?"}
C -->|"yes"| NG
C -->|"no"| D{"スコープがある ?"}
D -->|"no"| OK["名前から推測して繋ぐ"]
D -->|"yes"| E{"automatic_scope_inversing<br/>が true ?"}
E -->|"yes"| OK
E -->|"no"| NG
| 条件 | 自動推測 | 明示が要るか |
|---|---|---|
素の has_many :tasks |
される | 不要 |
-> { order(...) } 付き |
Rails 7.0+設定次第 | 迷ったら書く |
foreign_key: :owner_id 指定 |
されない | 必須 |
has_many :xxx, through: :yyy |
されない | そもそも inverse_of は使えない |
class_name だけ変更 |
される(as/クラス名から解決) |
不要 |
inverse_of: false |
しない(明示的な無効化) | ― |
:foreign_keyを書いた瞬間に自動推測が切れるのは見落としやすい。FK をカスタムしている関連は全部疑ってよい。
かつては「スコープが付いた関連は自動推測されない」で一律だった。理由は、スコープが親自身を除外してしまう可能性があるから。
# 相手側のスコープが親を弾くと、繋いだ結果が嘘になる
has_many :tasks, -> { where(archived: false) }
Rails 7.0 で config.active_record.automatic_scope_inversing が入り、判定が2方向に分かれた。
def scope_allows_automatic_inverse_of?(reflection, inverse_reflection)
if inverse_reflection
!reflection.scope # 相手側にスコープ → 常に NG
else
!reflection.scope || reflection.klass.automatic_scope_inversing # 自分側 → 設定次第
end
end
Project.has_many :tasks, -> { ... } から task.project を繋ぐ方向)は、設定が true なら許可される。Task.belongs_to :project の裏返しを探して has_many にスコープがある方向)は、今も一律で NG。automatic_scope_inversing は config.load_defaults 7.0 以上で true になる。新規アプリは有効だが、古いアプリを上げてきた場合は load_defaults が 6.x のままということがよくある。
# 現物で確認する
Task.automatic_scope_inversing # => true / false
Project.reflect_on_association(:tasks).inverse_of&.name # => :project なら繋がっている / nil なら繋がっていない
reflect_on_association(...).inverse_of が nil なら、そのまま上の①〜③を踏む。コンソールで1行叩けば確定するので、悩む前にこれを見る。
コストは「1行増える」だけで、書いて壊れることはない(inverse_of: false と違い、正しい関連名を書く限り挙動は自動推測時と同じ)。判断は次で足りる。
| 状況 | 方針 |
|---|---|
:through / :foreign_key 付き |
必ず書く(:through は書けないので設計側で対処) |
| スコープ付き | 書く。設定に依存させない |
| 素の関連 | 書かない。ノイズになる |
| 既存コードが書いている | 揃える。同じファイル内で有無が混ざる方が読みにくい |
inverse_of は関連名を書く。クラス名ではない。 inverse_of: :Project や inverse_of: :projects は解決に失敗する(belongs_to :project なら :project)。has_many :through には指定できない。 中間を経由するので裏返しが一意に決まらない。N+1 が気になるなら preload 側で対処する(eager_load・preload・includesの違いと使い分け)。as: の名前で推測される。 has_many :comments, as: :commentable なら相手の belongs_to :commentable が自動で見つかる。inverse_of: false は「無効化」であって「未指定」ではない。 意図的に切りたいとき以外は書かない。dependent: とは無関係。 同じ行に並ぶので混同しやすいが、dependent: は DB の削除の話、inverse_of: はメモリ上の参照の話。-> { } 側の話