active-record-associations について
Rails 開発者向けに、Active Record の関連、マイグレーション、先行読み込みの設計を支援するスキルです。外部キーの配置や依存削除、逆関連、データベース制約を扱い、孤立レコードや N+1 クエリの防止に役立ちます。
主な機能
- 01一般的な関連とポリモーフィック関連の設計
- 02依存削除と逆関連の設定
- 03外部キーとインデックスを含むマイグレーション
- 04先行読み込みによる N+1 クエリの削減
- 05関連とクエリ動作のデバッグ
活用シーン
- 01Rails モデル間の一対多・多対多関係の作成
- 02関連制約を含むマイグレーションの作成
- 03関連に起因する N+1 クエリの調査
- 04ポリモーフィック関連や自己参照関連の処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill active-record-associationsインストール後、Claude Code で/active-record-associationsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/association-types.mdcounter-caches.mdeager-loading.mdpolymorphic.mdself-referential.mdtesting.md詳細
Active Record の関連付けエキスパート
Rails モデル間の、正しく、パフォーマンスに優れ、保守しやすい関連付けを定義します。
基本方針
基本原則:
- 外部キー(FK)は常に
belongs_to側にあります。どちらに配置するか迷ったら、「どのテーブルに_idカラムがあるか?」と考えてください。そのモデルにbelongs_toを設定します。 - 常に
dependent:を設定します。孤立したレコードは、いつ問題を引き起こしてもおかしくありません。 has_and_belongs_to_manyよりhas_many :throughを優先します。例外はありません。HABTM は行き詰まりにつながり、後悔することになります。- デフォルトで双方向に設定します。両側を宣言してください。Rails が推測できない場合は
inverse_ofを使用します。 - 積極的に先読みします。N+1 クエリは、Rails アプリにおけるパフォーマンス低下の最大の原因です。
- モデルの関連付けはデータベース制約で補強します。外部キー、インデックス、一意制約は、モデルだけでなくマイグレーションにも設定します。
このスキルを使用する場面
- モデル間の関連付け(1対1、1対多、多対多)の定義
- 外部キーや結合テーブルを含むマイグレーションの作成
- N+1 クエリ問題の解決
- ポリモーフィックまたは自己参照関連の設定
has_many :throughと HABTM の選択- カウンターキャッシュの追加
- 関連付けに関するエラーのデバッグ
手順
ステップ 1:関連の種類を特定する
コードを書く前に、関連の種類を特定します。
| 関連 | 親モデル | 子モデル | 外部キーの場所 |
|---|---|---|---|
| 1対多 | has_many :children | belongs_to :parent | 子テーブル |
| 1対1 | has_one :child | belongs_to :parent | 子テーブル |
| 多対多 | has_many :others, through: :join | has_many :others, through: :join | 結合テーブル |
| ポリモーフィック | has_many :things, as: :thingable | belongs_to :thingable, polymorphic: true | 子テーブル(_id + _type) |
| 自己参照 | has_many :children, class_name: "Self" | belongs_to :parent, class_name: "Self" | 同じテーブル |
鉄則: 外部キーのカラムがあるテーブルのモデルに belongs_to を設定します。
ステップ 2:既存のパターンを確認する
まず既存のコードベースを確認します。既存のパターンから、従うべき規約が分かります。
# Find existing associations
rg "has_many\|has_one\|belongs_to\|has_and_belongs_to_many" app/models/
# Check existing migrations for FK patterns
rg "add_reference\|t.belongs_to\|t.references\|foreign_key" db/migrate/
# Look at schema
cat db/schema.rb | grep -A5 "create_table"
命名、dependent オプション、インデックスのパターンについて、既存のプロジェクト規約に合わせます。
ステップ 3:関連付けを記述する
1対多(最も一般的)
# app/models/author.rb
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
# app/models/book.rb
class Book < ApplicationRecord
belongs_to :author
end
マイグレーション:
class CreateBooks < ActiveRecord::Migration[8.1]
def change
create_table :books do |t|
t.references :author, null: false, foreign_key: true
t.string :title
t.timestamps
end
end
end
1対1
class User < ApplicationRecord
has_one :profile, dependent: :destroy
end
class Profile < ApplicationRecord
belongs_to :user
end
マイグレーション — 一意インデックスを追加します。
create_table :profiles do |t|
t.references :user, null: false, foreign_key: true, index: { unique: true }
t.timestamps
end
多対多(常に has_many :through を使用)
class Doctor < ApplicationRecord
has_many :appointments, dependent: :destroy
has_many :patients, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :doctor
belongs_to :patient
end
class Patient < ApplicationRecord
has_many :appointments, dependent: :destroy
has_many :doctors, through: :appointments
end
マイグレーション — 結合モデルには主キーを持つ専用のテーブルを作成します。
create_table :appointments do |t|
t.references :doctor, null: false, foreign_key: true
t.references :patient, null: false, foreign_key: true
t.datetime :scheduled_at
t.timestamps
end
ステップ 4:すべての関連付けに dependent: を設定する
すべての has_many と has_one に dependent: オプションが必要です。設定しないと、親を削除したときに子レコードが孤立し、外部キーが宙ぶらりんになります。
| オプション | 使用する場面 |
|---|---|
dependent: :destroy | デフォルトの選択肢です。 子に対してコールバックを実行します。子が独自の関連付けやコールバックを持つ場合に使用します。 |
dependent: :delete_all | パフォーマンスの最適化です。コールバックをスキップします。さらに関連付けを持たない末端ノードに使用します。 |
dependent: :nullify | 子レコードは保持し、関連付けだけを削除します。外部キーで NULL を許可する必要があります。 |
dependent: :restrict_with_error | 子が存在する場合は削除を防ぎます。ビジネスルールの適用に使用します。 |
dependent: :destroy_async | 大規模なデータセットに使用します。バックグラウンドジョブをキューに追加します。Active Job が必要です。データベースレベルの外部キー制約とは併用しないでください。 |
# Missing dependent — orphaned records when author is destroyed
has_many :books
# Fixed — children cleaned up properly
has_many :books, dependent: :destroy
belongs_to の場合: belongs_to に dependent: を設定しないでください。混乱を招き、循環削除につながる可能性があります。
ステップ 5:Eager Loading を処理する(N+1 を解消する)
問題:
# N+1 — fires 1 query for authors + N queries for books
Author.all.each { |a| puts a.books.count }
解決策 — includes を使用する:
# 2 queries total — always
Author.includes(:books).each { |a| puts a.books.count }
使い分け:
| メソッド | SQL 戦略 | 使用する場面 |
|---|---|---|
includes | スマートなデフォルトです。 条件で関連付けを参照するかどうかに応じて、preload または eager_load を使用します。 | ほとんどの場合はこちらを使います。まずはこれを使いましょう。 |
preload | 個別のクエリ(SELECT * FROM books WHERE author_id IN (...)) | 個別のクエリを使いたい場合です。絞り込みには使用できません。 |
eager_load | 単一の LEFT JOIN | WHERE または ORDER BY で関連付けられたカラムを使って絞り込みや並べ替えを行う必要がある場合です。 |
strict_loading | 遅延読み込み時に例外を発生させます | 開発中にモデルレベルで N+1 を防止します。 |
# Filter by association column — must use eager_load (or includes handles it)
Author.includes(:books).where(books: { published: true })
# Nested eager loading
Author.includes(books: :reviews)
# Strict loading on a model (development safety net)
class Author < ApplicationRecord
self.strict_loading_by_default = true # Rails 7+
has_many :books, dependent: :destroy
end
# Strict loading on a query
Author.strict_loading.all
ステップ 6:必要に応じて inverse_of を使う
Rails は単純なケースでは逆関連付けを自動検出します。次の場合は inverse_of が必要です。
:foreign_keyまたは:class_nameを使用する場合- スコープ付き関連付けがある場合
:through関連付けを使用する場合
# Rails CAN'T auto-detect this — specify inverse_of
class Author < ApplicationRecord
has_many :books, inverse_of: :writer
end
class Book < ApplicationRecord
belongs_to :writer, class_name: "Author", foreign_key: "author_id"
end
重要な理由: inverse_of が正しく設定されていないと、次の問題が発生します。
- 追加のクエリが発生する(すでに読み込み済みのデータに対する N+1)
- メモリ上のオブジェクトに一貫性がなくなる
- 新しいレコードで presence バリデーションが失敗する
ステップ 7:マイグレーションを作成する
すべての関連付けには、対応するマイグレーションが必要です。 関連付けを定義しても、データベースのカラムは作成されません。
必ず含めるもの:
- 外部キーのカラム —
t.referencesまたはadd_referenceを使います - データベースレベルの外部キー制約 —
foreign_key: true - インデックス —
t.referencesで自動的に作成されますが、必ず確認してください - NOT NULL 制約 — 関連付けが
optional: trueの場合を除きます
# New table
create_table :books do |t|
t.references :author, null: false, foreign_key: true
t.timestamps
end
# Adding to existing table
add_reference :books, :author, null: false, foreign_key: true
ポリモーフィック関連付けの場合:
t.references :commentable, polymorphic: true, null: false
# Creates: commentable_id (bigint) + commentable_type (string) + composite index
よくある間違いと修正方法
1. 間違ったテーブルに外部キーを置く
# Wrong — puts FK on authors table (authors don't have book_id!)
class Author < ApplicationRecord
belongs_to :book # ← backwards
end
# Correct — books table has author_id
class Book < ApplicationRecord
belongs_to :author
end
ルール: _id カラムを持つテーブルには belongs_to を設定します。
2. has_many に dependent: がない
# Destroys author, leaves orphaned books with dangling author_id
class Author < ApplicationRecord
has_many :books # ← missing dependent
end
# Fixed
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
3. has_many :through ではなく HABTM を使う
# Avoid — can't add attributes, callbacks, or validations to the join
class Student < ApplicationRecord
has_and_belongs_to_many :courses
end
# Better — flexible, extensible, future-proof
class Student < ApplicationRecord
has_many :enrollments, dependent: :destroy
has_many :courses, through: :enrollments
end
class Enrollment < ApplicationRecord
belongs_to :student
belongs_to :course
# Now you can add: grade, enrolled_at, status, etc.
end
4. 不要なポリモーフィック関連付け
ポリモーフィック関連付けを使う場合:
- 複数の無関係なモデルで同じ子モデル(コメント、添付ファイル、タグなど)が必要な場合
- フレームワーク/エンジンの機能を構築する場合
ポリモーフィック関連付けを使わない場合:
- 対象のモデルが 2~3 個のみの場合 — 代わりに個別の外部キーを使います
- データベースレベルの参照整合性が必要な場合(ポリモーフィック外部キーにはデータベース制約を設定できません)
- ポリモーフィックな境界をまたいで効率よく結合する必要がある場合
# Polymorphic is overkill here — just use two belongs_to
class Comment < ApplicationRecord
belongs_to :post # Better than polymorphic if only commenting on posts and articles
belongs_to :article
end
5. ビューでの N+1
# Controller — forgot to eager load
def index
@posts = Post.all # N+1 when view calls post.author.name
end
# Fixed
def index
@posts = Post.includes(:author, :comments)
end
6. 理由のない optional: true
# belongs_to validates presence by default (Rails 5+)
# Don't add optional: true unless the FK is genuinely nullable
belongs_to :author # Validates author exists ✓
belongs_to :author, optional: true # Only if author_id can be NULL
クイックリファレンス
関連付けの早見表
# One-to-many
has_many :posts, dependent: :destroy
belongs_to :user
# One-to-one
has_one :profile, dependent: :destroy
belongs_to :user
# Many-to-many (through)
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
# Polymorphic
has_many :comments, as: :commentable, dependent: :destroy
belongs_to :commentable, polymorphic: true
# Self-referential
has_many :subordinates, class_name: "Employee", foreign_key: "manager_id", dependent: :nullify
belongs_to :manager, class_name: "Employee", optional: true
# Counter cache (declare on belongs_to side, column on has_many side)
belongs_to :author, counter_cache: true
# Requires: add_column :authors, :books_count, :integer, default: 0, null: false
# Scoped association
has_many :published_books, -> { where(published: true) }, class_name: "Book", dependent: :destroy
# With ordering
has_many :books, -> { order(created_at: :desc) }, dependent: :destroy
マイグレーションのパターン
# Standard FK
t.references :author, null: false, foreign_key: true
# Polymorphic FK
t.references :commentable, polymorphic: true, null: false
# Self-referential FK
t.references :manager, foreign_key: { to_table: :employees }
# Custom FK name
t.bigint :creator_id
add_foreign_key :posts, :users, column: :creator_id
# Join table for has_many :through
create_table :enrollments do |t|
t.references :student, null: false, foreign_key: true
t.references :course, null: false, foreign_key: true
t.timestamps
end
add_index :enrollments, [:student_id, :course_id], unique: true
Eager Loading の判断フロー
Need to filter/order by associated columns?
YES → includes (auto-detects) or eager_load (explicit LEFT JOIN)
NO → includes (auto-detects) or preload (explicit separate queries)
Need nested associations?
→ Author.includes(books: [:reviews, :publisher])
Want to prevent N+1 entirely?
→ strict_loading (model or query level)
belongs_to オプションのクイックリファレンス
| オプション | デフォルト | 目的 |
|---|---|---|
optional: true | false | 外部キーの NULL を許可する |
counter_cache: true | false | 親のカウントカラムを維持する |
touch: true | false | 保存時に親の updated_at を更新する |
polymorphic: true | false | ポリモーフィック関連付け |
class_name: | 推論されます | 標準外のクラスを指定する |
foreign_key: | "#{name}_id" | 標準外の外部キーカラムを指定する |
inverse_of: | 自動検出 | 逆関連付けの名前を指定する |
has_many / has_one オプションのクイックリファレンス
| オプション | デフォルト | 目的 |
|---|---|---|
dependent: | なし | 必ず設定してください。 ステップ 4 を参照してください。 |
class_name: | 推論されます | 標準外のクラス名 |
foreign_key: | "#{model}_id" | 標準外の外部キーカラム |
inverse_of: | 自動検出 | 逆関連付けを指定する |
through: | なし | 中間関連付け |
source: | 推論されます | :through のソース関連付け名 |
as: | なし | ポリモーフィックインターフェース名 |
counter_cache: | なし | (has_many のみ)カウンターキャッシュカラムを読み込む |
関連付けのデバッグ
# Check what associations a model has
Author.reflect_on_all_associations.map { |a| [a.macro, a.name] }
# Check a specific association
Author.reflect_on_association(:books)
# See the SQL an association generates
Author.first.books.to_sql
# Check if inverse is set
Author.reflect_on_association(:books).inverse_of
# Find orphaned records
Book.left_joins(:author).where(authors: { id: nil })
# Reset counter cache
Author.reset_counters(author_id, :books)
避けるべきアンチパターン
has_many/has_oneにdependent:を指定しない — 孤立したレコードが、原因を特定しにくいデータの不具合を引き起こします- HABTM — 代わりに
has_many :throughを使います。HABTM では、結合に属性、バリデーション、コールバックを持たせられません - データベース制約の欠落 — マイグレーションで
foreign_key: trueを指定します - ループ内での遅延読み込み —
includes/preload/eager_loadを使います - 2 つのモデルに対するポリモーフィック関連付け — 外部キーを個別に使います
- デフォルトでの
optional: true— 外部キーが実際に NULL を許容する場合にのみ指定します inverse_ofの指定漏れ — カスタムの:foreign_keyまたは:class_nameを使う場合に指定しますhas_oneの外部キーに一意インデックスを付けない — 付けないと重複レコードが許可されます- マイグレーションを忘れる — 関連付けによってカラムが作成されることはありません
- 大規模なコレクションに
dependent: :destroyを指定する —:delete_allまたは:destroy_asyncを使います
詳しいパターン、オプションのリファレンス、高度な例については、references/ ディレクトリを参照してください。
references/association-types.md— belongs_to、has_one、has_many、has_many :through、HABTM の詳しい解説references/polymorphic.md— ポリモーフィック関連付け、STI、委譲型references/self-referential.md— ツリー構造、マネージャーと従業員、ソーシャルフォローreferences/eager-loading.md— includes/preload/eager_load の各戦略、inverse_ofreferences/counter-caches.md— カウンターキャッシュ、スコープ付き関連付け、コールバック、拡張references/testing.md— テストパターン、マイグレーションの例、パフォーマンスのヒント、トラブルシューティング