Active Record associations in Rails: a practical tutorial
Active Record associations let you declare relationships between models in plain Ruby, without writing SQL. Rails uses those declarations to give you a rich set of methods for working with related records. Getting the right association in place for each relationship is one of the most consequential design decisions in a Rails application.
Belongs_to
belongs_to sets up a one-to-one connection where the foreign key lives on this model’s table. If a Book belongs to an Author, the books table needs an author_id column.
class Book < ApplicationRecord
belongs_to :author
end
The association name must be singular. Rails infers the class name from it (singularizes author → Author) and looks for author_id on the current model’s table. If either convention doesn’t fit, you can override them:
class Book < ApplicationRecord
belongs_to :author, foreign_key: :writer_id
end
This tells Rails to use the writer_id column instead of author_id. Use class_name when the association name doesn’t match the actual class:
class Book < ApplicationRecord
belongs_to :author, class_name: 'Writer'
end
Declaring belongs_to :author also generates methods: author, author=, build_author, create_author, and reload_author.
Has_one
has_one also represents a one-to-one relationship, but the foreign key lives on the other model’s table. A Supplier has one Account, so the accounts table carries the supplier_id column.
class Supplier < ApplicationRecord
has_one :account
end
The table structure reflects which side holds the key:
create_table :accounts do |t|
t.belongs_to :supplier, index: true, unique: true
end
Has_many
has_many is the “one-to-many” side. An Author has many Book records. The name is always pluralized — unlike belongs_to, which requires singular.
class Author < ApplicationRecord
has_many :books
end
Because has_many is plural, Rails looks for a author_id column on the books table automatically. Beyond basic getter and setter methods, a has_many association generates an entire collection proxy: books <<, books.delete, books.destroy, books.empty?, books.size, books.find, books.build, books.create.
Has_many :through
Use has_many :through when you need a many-to-many relationship via an intermediate model. A physician has many patients through appointments, for example.
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
The join model (Appointment) is a real Active Record model with its own table, which means it can have validations, callbacks, and extra attributes. Choose has_many :through over has_and_belongs_to_many whenever you need that flexibility.
When you assign to a through-collection, Rails manages the join rows automatically:
physician.patients = patients
New join rows are created for newly associated patients. Orphan join rows are deleted. One thing to watch: join row deletion happens with direct SQL, so callbacks on the join model do not fire.
The source option
When the name of the association on the join model doesn’t match what you’re calling it on the parent, use source to point Rails in the right direction:
class Document < ApplicationRecord
has_many :sections
has_many :paragraphs, through: :sections
end
class Section < ApplicationRecord
belongs_to :document
has_many :paragraphs
end
Document.first.paragraphs follows the chain document → sections → paragraphs. If the association on Section were named texts instead of paragraphs, you’d write source: :texts on the through declaration.
Has_one :through
has_one :through gives you a one-to-one relationship through an intermediate model. A supplier has one account, and through that account it has one account history:
class Supplier < ApplicationRecord
has_one :account
has_one :account_history, through: :account
end
Has_and_belongs_to_many
For simple many-to-many relationships where the join has no extra data, HABTM avoids the overhead of a full join model:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
HABTM requires a join table with no primary key. Rails convention names it from the two table names sorted alphabetically:
create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly, index: true
t.belongs_to :part, index: true
end
Use HABTM only when the join has no behaviour of its own. As soon as you need validations or extra columns, promote it to a proper join model and use has_many :through.
Dependent options
When you destroy a record, what happens to its associated records? The dependent option on has_many and has_one controls this.
dependent: :destroy calls destroy on each associated record, which runs callbacks and validations:
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
This is thorough but expensive for large collections because every associated record gets instantiated and processed individually.
dependent: :delete_all sends a direct DELETE SQL statement per record without instantiating them. Callbacks are skipped, which makes it faster but less safe if you rely on before_destroy hooks:
class Author < ApplicationRecord
has_many :books, dependent: :delete_all
end
dependent: :nullify sets all foreign keys to NULL without destroying any records:
class Author < ApplicationRecord
has_many :books, dependent: :nullify
end
This is the right choice for polymorphic associations, where nullifying both the _id and _type columns avoids the expensive nested delete problem.
restrict_with_error and restrict_with_exception prevent deletion entirely if associated records exist, which is useful for enforcing referential integrity at the application level.
Polymorphic associations
Sometimes a single association needs to point to different models. Pictures might belong to either an Employee or a Product. Polymorphic associations solve this with a _type column that stores the class name alongside the _id:
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
The migration uses references with polymorphic: true:
create_table :pictures do |t|
t.references :imageable, polymorphic: true, index: true
end
# Creates: imageable_id (integer) + imageable_type (string)
Now @picture.imageable returns whichever parent object is actually stored there. One limitation: polymorphic associations cannot use inverse_of because the actual associated class changes at runtime.
Avoiding n+1 queries with includes
Loading a list of authors then accessing their books triggers one query per author — the classic N+1 problem:
# N+1: 1 query for authors + N queries for books
authors = Author.all
authors.each { |a| a.books.map(&:title) }
Fix it by eager-loading the associated records with includes:
authors = Author.includes(:books)
authors.each { |a| a.books.map(&:title) } # 2 queries total
Rails loads all books in a single second query using IN with the author IDs. For nested associations:
Author.includes(books: [:author, :publisher])
This pattern works with polymorphic associations too:
Picture.includes(:imageable).where(...)
Self-referential associations
A model can reference itself. Employees have a manager who is also an employee, or users can have friends who are also users:
class Employee < ApplicationRecord
belongs_to :manager, class_name: 'Employee', foreign_key: 'manager_id'
has_many :subordinates, class_name: 'Employee', foreign_key: 'manager_id'
end
foreign_key tells Rails which column holds the reference on the other side. class_name resolves the ambiguity that self-reference creates.
For a many-to-many self-referential relationship like friendships, you need a join table and a proper join model:
class User < ApplicationRecord
has_many :friendships
has_many :friends, through: :friendships, source: :friend
end
class Friendship < ApplicationRecord
belongs_to :user
belongs_to :friend, class_name: 'User'
end
The friendships table needs both user_id and friend_id columns. The source: :friend option tells Rails to look for a friend association on the Friendship model rather than assuming friends.
Bi-directional associations and inverse_of
When two associations point to the same record, you can hint this to Rails with inverse_of:
class Author < ApplicationRecord
has_many :books, inverse_of: :author
end
class Book < ApplicationRecord
belongs_to :author, inverse_of: :books
end
Without inverse_of, modifying @author.book.title and then @book.author.name in the same request can cause unexpected behaviour because Rails may have loaded two separate instances of the same record. With inverse_of set, Rails keeps both sides in sync automatically.
Conclusion
Associations are the connective tissue of a Rails application’s data model. belongs_to and has_one handle one-to-one relationships, has_many covers one-to-many, and has_many :through handles any many-to-many relationship that needs a real join model. Polymorphic associations let a single association point to multiple model types without duplicating columns.
The choices you make with dependent, inverse_of, and eager loading with includes have real consequences at runtime. Getting them right from the start keeps your application predictable as it grows.
See also
- ActiveRecord Basics — understand the model layer before working with associations
- Rails Routing — how routing and model associations interact
- Ruby Blocks and Iterators — the collection methods that associations generate are built on blocks