| name | active-record-associations |
| description | Expert guidance for defining and using Active Record associations in Rails. Use when creating associations, defining model relationships, troubleshooting foreign keys, fixing N+1 queries, setting up belongs_to, has_many, has_one, has_many :through, polymorphic associations, self-referential joins, join tables, counter caches, dependent options, inverse_of, eager loading, or any question about how models relate to each other. |
| allowed-tools | Read, Grep, Glob, Write, Edit, Bash(bin/rails generate*), Bash(bin/rails db:migrate*), Bash(bin/rails console*) |
Active Record Associations Expert
Define correct, performant, and maintainable associations between Rails models.
Philosophy
Core Principles:
- The FK lives on the
belongs_to side — always. If you're confused about where it goes, ask "which table has the _id column?" That model gets belongs_to.
- Always set
dependent: — orphaned records are bugs waiting to happen.
- Prefer
has_many :through over has_and_belongs_to_many — every time. HABTM is a dead end you'll regret.
- Bi-directional by default — declare both sides. Use
inverse_of when Rails can't infer it.
- Eager load aggressively — N+1 queries are the #1 performance killer in Rails apps.
- Database constraints back up model associations — foreign keys, indexes, and unique constraints belong in migrations, not just models.
When To Use This Skill
- Defining relationships between models (one-to-one, one-to-many, many-to-many)
- Creating migrations with foreign keys and join tables
- Fixing N+1 query problems
- Setting up polymorphic or self-referential associations
- Choosing between
has_many :through and HABTM
- Adding counter caches
- Debugging association-related errors
Instructions
Step 1: Identify the Relationship Type
Before writing any code, determine the relationship:
| Relationship | Parent Model | Child Model | FK Location |
|---|
| One-to-many | has_many :children | belongs_to :parent | Child table |
| One-to-one | has_one :child | belongs_to :parent | Child table |
| Many-to-many | has_many :others, through: :join | has_many :others, through: :join | Join table |
| Polymorphic | has_many :things, as: :thingable | belongs_to :thingable, polymorphic: true | Child table (_id + _type) |
| Self-referential | has_many :children, class_name: "Self" | belongs_to :parent, class_name: "Self" | Same table |
The golden rule: belongs_to goes on the model whose table has the foreign key column.
Step 2: Check Existing Patterns
Look at the existing codebase first — existing patterns tell you what conventions to follow:
rg "has_many\|has_one\|belongs_to\|has_and_belongs_to_many" app/models/
rg "add_reference\|t.belongs_to\|t.references\|foreign_key" db/migrate/
cat db/schema.rb | grep -A5 "create_table"
Match existing project conventions for naming, dependent options, and index patterns.
Step 3: Write the Association
One-to-Many (most common)
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
class Book < ApplicationRecord
belongs_to :author
end
Migration:
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
One-to-One
class User < ApplicationRecord
has_one :profile, dependent: :destroy
end
class Profile < ApplicationRecord
belongs_to :user
end
Migration — add a unique index:
create_table :profiles do |t|
t.references :user, null: false, foreign_key: true, index: { unique: true }
t.timestamps
end
Many-to-Many (always use 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
Migration — the join model gets its own table with a primary key:
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
Step 4: Set dependent: on Every Association
Every has_many and has_one needs a dependent: option — without one, deleting a parent leaves orphaned child records with dangling foreign keys.
| Option | When to Use |
|---|
dependent: :destroy | Default choice. Runs callbacks on children. Use when children have their own associations or callbacks. |
dependent: :delete_all | Performance optimization. Skips callbacks. Use for leaf nodes with no further associations. |
dependent: :nullify | Keep the child records but remove the association. FK must allow NULL. |
dependent: :restrict_with_error | Prevent deletion if children exist. Business rule enforcement. |
dependent: :destroy_async | Large datasets. Enqueues background job. Requires Active Job. Don't use with DB-level FK constraints. |
has_many :books
has_many :books, dependent: :destroy
On belongs_to: Don't set dependent: on belongs_to — it causes confusion and can lead to circular destruction.
Step 5: Handle Eager Loading (Kill N+1)
The Problem:
Author.all.each { |a| puts a.books.count }
The Fix — use includes:
Author.includes(:books).each { |a| puts a.books.count }
When to use which:
| Method | SQL Strategy | Use When |
|---|
includes | Smart default. Uses preload or eager_load depending on whether you reference the association in conditions. | Most cases. Start here. |
preload | Separate queries (SELECT * FROM books WHERE author_id IN (...)) | You want separate queries. Can't use for filtering. |
eager_load | Single LEFT JOIN | You need to filter/order by associated columns in WHERE or ORDER BY. |
strict_loading | Raises if lazy-loaded | Prevent N+1 at the model level during development. |
Author.includes(:books).where(books: { published: true })
Author.includes(books: :reviews)
class Author < ApplicationRecord
self.strict_loading_by_default = true
has_many :books, dependent: :destroy
end
Author.strict_loading.all
Step 6: Use inverse_of When Needed
Rails auto-detects inverse associations in simple cases. You need inverse_of when:
- You use
:foreign_key or :class_name
- You have scoped associations
- You use
:through associations
class Author < ApplicationRecord
has_many :books, inverse_of: :writer
end
class Book < ApplicationRecord
belongs_to :writer, class_name: "Author", foreign_key: "author_id"
end
Why it matters: Without correct inverse_of, you get:
- Extra queries (N+1 for already-loaded data)
- Inconsistent in-memory objects
- Failed presence validations on new records
Step 7: Write the Migration
Every association needs a corresponding migration. Associations don't create database columns.
Always include:
- Foreign key column — use
t.references or add_reference
- Database-level foreign key constraint —
foreign_key: true
- Index — automatic with
t.references, but verify
- NOT NULL constraint — unless the association is
optional: true
create_table :books do |t|
t.references :author, null: false, foreign_key: true
t.timestamps
end
add_reference :books, :author, null: false, foreign_key: true
For polymorphic:
t.references :commentable, polymorphic: true, null: false
Common Mistakes (and Fixes)
1. FK on the Wrong Table
class Author < ApplicationRecord
belongs_to :book
end
class Book < ApplicationRecord
belongs_to :author
end
Rule: The table with the _id column gets belongs_to.
2. Missing dependent: on has_many
class Author < ApplicationRecord
has_many :books
end
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
3. Using HABTM Instead of has_many :through
class Student < ApplicationRecord
has_and_belongs_to_many :courses
end
class Student < ApplicationRecord
has_many :enrollments, dependent: :destroy
has_many :courses, through: :enrollments
end
class Enrollment < ApplicationRecord
belongs_to :student
belongs_to :course
end
4. Polymorphic When You Shouldn't
Use polymorphic when:
- Multiple unrelated models need the same child (comments, attachments, tags)
- You're building a framework/engine feature
Don't use polymorphic when:
- Only 2-3 specific models — use separate FKs instead
- You need database-level referential integrity (polymorphic FKs can't have DB constraints)
- You need to join across the polymorphic boundary efficiently
class Comment < ApplicationRecord
belongs_to :post
belongs_to :article
end
5. N+1 in Views
def index
@posts = Post.all
end
def index
@posts = Post.includes(:author, :comments)
end
6. optional: true Without Reason
belongs_to :author
belongs_to :author, optional: true
Quick Reference
Association Cheat Sheet
has_many :posts, dependent: :destroy
belongs_to :user
has_one :profile, dependent: :destroy
belongs_to :user
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
has_many :comments, as: :commentable, dependent: :destroy
belongs_to :commentable, polymorphic: true
has_many :subordinates, class_name: "Employee", foreign_key: "manager_id", dependent: :nullify
belongs_to :manager, class_name: "Employee", optional: true
belongs_to :author, counter_cache: true
has_many :published_books, -> { where(published: true) }, class_name: "Book", dependent: :destroy
has_many :books, -> { order(created_at: :desc) }, dependent: :destroy
Migration Patterns
t.references :author, null: false, foreign_key: true
t.references :commentable, polymorphic: true, null: false
t.references :manager, foreign_key: { to_table: :employees }
t.bigint :creator_id
add_foreign_key :posts, :users, column: :creator_id
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 Decision Tree
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 Options Quick Reference
| Option | Default | Purpose |
|---|
optional: true | false | Allow NULL foreign key |
counter_cache: true | false | Maintain count column on parent |
touch: true | false | Update parent's updated_at on save |
polymorphic: true | false | Polymorphic association |
class_name: | Inferred | Specify non-standard class |
foreign_key: | "#{name}_id" | Specify non-standard FK column |
inverse_of: | Auto-detected | Specify inverse association name |
has_many / has_one Options Quick Reference
| Option | Default | Purpose |
|---|
dependent: | None | Always set this. See Step 4. |
class_name: | Inferred | Non-standard class name |
foreign_key: | "#{model}_id" | Non-standard FK column |
inverse_of: | Auto-detected | Specify inverse association |
through: | None | Through association |
source: | Inferred | Source association name for :through |
as: | None | Polymorphic interface name |
counter_cache: | None | (has_many only) read counter cache column |
Debugging Associations
Author.reflect_on_all_associations.map { |a| [a.macro, a.name] }
Author.reflect_on_association(:books)
Author.first.books.to_sql
Author.reflect_on_association(:books).inverse_of
Book.left_joins(:author).where(authors: { id: nil })
Author.reset_counters(author_id, :books)
Anti-Patterns to Avoid
- No
dependent: on has_many/has_one — orphaned records cause subtle data bugs
- HABTM — use
has_many :through instead; HABTM can't hold attributes, validations, or callbacks on the join
- Missing database constraints —
foreign_key: true in migrations
- Lazy loading in loops — use
includes / preload / eager_load
- Polymorphic for 2 models — just use separate foreign keys
optional: true by default — only when the FK is genuinely nullable
- Missing
inverse_of — when using custom :foreign_key or :class_name
- No unique index on
has_one FK — allows duplicate records without it
- Forgetting the migration — associations don't create columns
dependent: :destroy on huge collections — use :delete_all or :destroy_async
For detailed patterns, options reference, and advanced examples, see the references/ directory:
references/association-types.md — Deep dive into belongs_to, has_one, has_many, has_many :through, HABTM
references/polymorphic.md — Polymorphic associations, STI, and delegated types
references/self-referential.md — Tree structures, manager-employee, social follows
references/eager-loading.md — includes/preload/eager_load strategies, inverse_of
references/counter-caches.md — Counter caches, scoped associations, callbacks, extensions
references/testing.md — Testing patterns, migration examples, performance tips, troubleshooting