mcpField
A
active-record-associations

active-record-associations

github.com/thinkoodle開発7177 回インストール8 週間前に更新
リポジトリ

active-record-associations について

Rails 開発者向けに、Active Record の関連、マイグレーション、先行読み込みの設計を支援するスキルです。外部キーの配置や依存削除、逆関連、データベース制約を扱い、孤立レコードや N+1 クエリの防止に役立ちます。

主な機能

  1. 01一般的な関連とポリモーフィック関連の設計
  2. 02依存削除と逆関連の設定
  3. 03外部キーとインデックスを含むマイグレーション
  4. 04先行読み込みによる N+1 クエリの削減
  5. 05関連とクエリ動作のデバッグ

活用シーン

  1. 01Rails モデル間の一対多・多対多関係の作成
  2. 02関連制約を含むマイグレーションの作成
  3. 03関連に起因する N+1 クエリの調査
  4. 04ポリモーフィック関連や自己参照関連の処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill active-record-associations

インストール後、Claude Code で/active-record-associationsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
association-types.md
counter-caches.md
eager-loading.md
polymorphic.md
self-referential.md
testing.md

詳細

Active Record の関連付けエキスパート

Rails モデル間の、正しく、パフォーマンスに優れ、保守しやすい関連付けを定義します。

基本方針

基本原則:

  1. 外部キー(FK)は常に belongs_to 側にあります。どちらに配置するか迷ったら、「どのテーブルに _id カラムがあるか?」と考えてください。そのモデルに belongs_to を設定します。
  2. 常に dependent: を設定します。孤立したレコードは、いつ問題を引き起こしてもおかしくありません。
  3. has_and_belongs_to_many より has_many :through を優先します。例外はありません。HABTM は行き詰まりにつながり、後悔することになります。
  4. デフォルトで双方向に設定します。両側を宣言してください。Rails が推測できない場合は inverse_of を使用します。
  5. 積極的に先読みします。N+1 クエリは、Rails アプリにおけるパフォーマンス低下の最大の原因です。
  6. モデルの関連付けはデータベース制約で補強します。外部キー、インデックス、一意制約は、モデルだけでなくマイグレーションにも設定します。

このスキルを使用する場面

  • モデル間の関連付け(1対1、1対多、多対多)の定義
  • 外部キーや結合テーブルを含むマイグレーションの作成
  • N+1 クエリ問題の解決
  • ポリモーフィックまたは自己参照関連の設定
  • has_many :through と HABTM の選択
  • カウンターキャッシュの追加
  • 関連付けに関するエラーのデバッグ

手順

ステップ 1:関連の種類を特定する

コードを書く前に、関連の種類を特定します。

関連親モデル子モデル外部キーの場所
1対多has_many :childrenbelongs_to :parent子テーブル
1対1has_one :childbelongs_to :parent子テーブル
多対多has_many :others, through: :joinhas_many :others, through: :join結合テーブル
ポリモーフィックhas_many :things, as: :thingablebelongs_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 JOINWHERE または 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:マイグレーションを作成する

すべての関連付けには、対応するマイグレーションが必要です。 関連付けを定義しても、データベースのカラムは作成されません。

必ず含めるもの:

  1. 外部キーのカラム — t.references または add_reference を使います
  2. データベースレベルの外部キー制約 — foreign_key: true
  3. インデックス — t.references で自動的に作成されますが、必ず確認してください
  4. 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: truefalse外部キーの NULL を許可する
counter_cache: truefalse親のカウントカラムを維持する
touch: truefalse保存時に親の updated_at を更新する
polymorphic: truefalseポリモーフィック関連付け
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)

避けるべきアンチパターン

  1. has_many/has_one に dependent: を指定しない — 孤立したレコードが、原因を特定しにくいデータの不具合を引き起こします
  2. HABTM — 代わりに has_many :through を使います。HABTM では、結合に属性、バリデーション、コールバックを持たせられません
  3. データベース制約の欠落 — マイグレーションで foreign_key: true を指定します
  4. ループ内での遅延読み込み — includes / preload / eager_load を使います
  5. 2 つのモデルに対するポリモーフィック関連付け — 外部キーを個別に使います
  6. デフォルトでの optional: true — 外部キーが実際に NULL を許容する場合にのみ指定します
  7. inverse_of の指定漏れ — カスタムの :foreign_key または :class_name を使う場合に指定します
  8. has_one の外部キーに一意インデックスを付けない — 付けないと重複レコードが許可されます
  9. マイグレーションを忘れる — 関連付けによってカラムが作成されることはありません
  10. 大規模なコレクションに 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_of
  • references/counter-caches.md — カウンターキャッシュ、スコープ付き関連付け、コールバック、拡張
  • references/testing.md — テストパターン、マイグレーションの例、パフォーマンスのヒント、トラブルシューティング

コメント