rubyguides

Rails associations: belongs_to, has_many, and through relationships

Rails associations let you declare relationships between models in plain Ruby, without writing SQL. Active Record associations are the foundation of every Rails data model. 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 important design decisions in a Rails application.

Intro context

Associations are how Rails models talk to each other. Instead of manually joining tables every time you need related data, you tell Rails how the records connect and let the framework build the convenience methods for you.

That approach matters because relationships show up everywhere in a real app: authors and books, suppliers and accounts, users and friendships, or pictures that can belong to more than one kind of record. Once you know the core association types, you can model those relationships cleanly without reaching for custom SQL too early.

TL;DR

  • Use belongs_to when the foreign key lives on the current table.
  • Use has_one for a one-to-one relationship where the foreign key lives on the other table.
  • Use has_many for a one-to-many relationship.
  • Use has_many :through when the join itself needs behavior or extra data.
  • Use polymorphic associations when one association needs to point at multiple model types.

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 — belongs_to :author, not belongs_to :authors. Rails infers the class name from the association name and looks for author_id on the current model’s table. If either convention does not fit your schema, you can override both with class_name and foreign_key options.

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 does not 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. These convenience methods handle the common operations you would otherwise write by hand, such as fetching the associated record or building a new one with the foreign key already set.

If you are deciding between belongs_to and has_many, start by asking which table should own the foreign key. That one question usually tells you which side should declare the association.

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_one is a good fit when the related object is truly singular and the relationship reads naturally from the parent side. Like belongs_to, it creates convenience methods on the declaring model, but the foreign key column lives on the associated table rather than on the declaring model’s table. This distinction matters when you are designing migration files.

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 an 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.

Those collection methods are where has_many becomes especially useful. They let you append, remove, inspect, and build associated records without dropping down to manual foreign-key management.

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.

That extra model is the reason has_many :through is so common in real applications. It gives the relationship a place to grow when the join needs timestamps, status flags, or domain-specific behavior.

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 does not match what you are 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 would write source: :texts on the through declaration.

Use source whenever the association name on the join model and the association name you want on the parent do not match. It keeps the relationship readable without forcing you to rename the underlying model method.

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

This pattern is less common than has_many :through, but it is useful when a one-to-one relationship lives behind another model and you want to expose it directly from the parent. For example, a Supplier that has one Account and through that account has one AccountHistory can access the history directly without navigating through the account first.

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 behavior of its own. As soon as you need validations or extra columns, promote it to a proper join model and use has_many :through. The extra join model adds more setup, but it gives you room to grow later if the relationship needs timestamps or validations.

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.

Choose the dependent strategy based on the cost of losing the records. If the associated rows are only meaningful inside the parent, :destroy may be correct. If they are shared or expensive to recreate, :nullify or a restrictive option may be safer.

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.

Polymorphic associations are powerful, but they also make queries and constraints a little more complex. Use them when the shared interface is genuinely broad, not just because the models happen to feel similar in one place.

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(...)

If you are working with a page that renders many parent and child rows together, includes is often the difference between a fast page and a slow one. The pattern is small, but the runtime effect is large.

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.

Self-referential associations can look odd at first, but they solve a real modelling problem: one table representing a relationship between rows in the same table. Once you see the class_name and foreign_key options together, the pattern becomes much easier to read.

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.

That is especially helpful in forms and nested object graphs, where the same row may be read through more than one association chain during a single request.

Frequently asked questions

When should I use has_many :through instead of HABTM?

Use has_many :through when the join needs validations, callbacks, timestamps, or any other behavior of its own. Use HABTM only when the relationship is truly just a direct many-to-many join.

How do I know which side should use belongs_to?

The side with the foreign key usually uses belongs_to. If a Book row stores author_id, then Book belongs to Author.

Why use inverse_of?

inverse_of helps Rails keep both sides of the same association aligned in memory. That can prevent subtle bugs when you are reading and writing related records during one request.

Once you are comfortable with associations, the next step is usually to combine them with validations, nested forms, and query patterns so related records stay consistent from the controller all the way down to the database.

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