rubyguides

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 authorAuthor) 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