Associations
Reviewed & published by Brayan K
Real apps are made of related data: authors and books, posts and comments, doctors and patients. Rails associations let you describe those relationships once and get a rich set of helper methods for free.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll wire up has_many, belongs_to, has_many :through, and has_and_belongs_to_many, and understand foreign keys and dependent: :destroy.
What You'll Learn in This Lesson
- One-to-many with has_many and belongs_to
- Where the foreign key column lives
- dependent: :destroy and related options
- Many-to-many with has_many :through
- has_and_belongs_to_many (HABTM)
- The helper methods associations generate
1️⃣ One-to-Many: has_many and belongs_to
The most common association is one-to-many. The "one" side declares has_many; the "many" side declares belongs_to and holds the foreign key column (author_id). Add dependent: :destroy so children don't outlive their parent.
# A one-to-many association: one Author writes many Books.
# The foreign key (author_id) lives on the "belongs_to" side (books).
class Author < ApplicationRecord
# The collection side. dependent: :destroy deletes an author's
# books when the author is destroyed.
has_many :books, dependent: :destroy
end
class Book < ApplicationRecord
# The single side. This is where the author_id column lives.
belongs_to :author
end
# The matching migration created books.author_id:
#
# create_table :books do |t|
# t.string :title
# t.references :author, foreign_key: true # adds author_id
# t.timestamps
# end
#
# Now in the rails console you get helper methods for free:
#
# author = Author.create(name: "Le Guin")
# author.books.create(title: "A Wizard of Earthsea")
# author.books.count # => 1
# book.author.name # => "Le Guin"2️⃣ Many-to-Many: :through and HABTM
When records relate on both sides, use a many-to-many. Prefer has_many :through when the join needs its own data; reach for has_and_belongs_to_many only for a plain link with nothing extra to store.
# Many-to-many WITH extra data: use has_many :through.
# A Doctor and a Patient meet through Appointments, and the
# appointment itself carries data (the date).
class Doctor < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Patient < ApplicationRecord
has_many :appointments
has_many :doctors, through: :appointments
end
# The JOIN model belongs_to BOTH sides and can have its own columns.
class Appointment < ApplicationRecord
belongs_to :doctor
belongs_to :patient
# appointments table also has an :appointment_date column
end
# Usage:
# doctor.patients # patients seen, via appointments
# doctor.appointments.first.appointment_date
# ---------------------------------------------------------------
# Simpler many-to-many with NO extra data and NO join model:
# has_and_belongs_to_many (HABTM) over a join table authors_books.
class AuthorHabtm < ApplicationRecord
has_and_belongs_to_many :books
end
class BookHabtm < ApplicationRecord
has_and_belongs_to_many :authors
endYour turn. Complete a blog's Post and Comment models so the one-to-many works end to end.
Mini-Challenge: A Tagging System
Model tags with has_many :through: a Post and a Tag joined by a Tagging. Write all three model classes.
# 🎯 MINI-CHALLENGE: a tagging system with has_many :through.
#
# 1. A Post has many Taggings, and has many Tags THROUGH taggings.
# 2. A Tag has many Taggings, and has many Posts THROUGH taggings.
# 3. The Tagging join model belongs_to :post and belongs_to :tag.
#
# Write the three model classes below. Then in the console you should
# be able to call post.tags and tag.posts.
#
# ✅ Goal:
# post.tags << Tag.create(name: "ruby")
# post.tags.map(&:name) # => ["ruby"]
# your code hereCommon Errors (and the fix)
- Missing foreign key — belongs_to needs a _id column; add it with t.references :author in a migration.
- Putting belongs_to on the wrong model — It goes on the table that holds the foreign key, not the one with has_many.
- Orphaned children after delete — Without dependent: :destroy, deleting a parent can leave dangling rows.
- Reaching for HABTM too soon — If the join ever needs its own data, you'll want has_many :through instead.
- Pluralisation typos — has_many takes a plural symbol (:books), belongs_to a singular one (:author).
Pro Tips
- 💡 belongs_to is required by default: since Rails 5, the parent must be present unless you say optional: true.
- 💡 Use inverse_of: it lets the two ends share one in-memory object and avoids extra queries.
- 💡 Index your foreign keys: t.references adds an index automatically, which keeps lookups fast.
📋 Quick Reference — Associations
| Relationship | Declaration |
|---|---|
| One-to-many (one side) | has_many :books |
| One-to-many (many side) | belongs_to :author |
| Delete children too | dependent: :destroy |
| Many-to-many (join model) | has_many :tags, through: :taggings |
| Many-to-many (no model) | has_and_belongs_to_many :books |
| Foreign key (migration) | t.references :author |
🎉 Lesson Complete!
- ✅ has_many / belongs_to model one-to-many relationships
- ✅ The foreign key (author_id) lives on the belongs_to side
- ✅ dependent: :destroy removes children with their parent
- ✅ has_many :through joins with an explicit model that can carry data
- ✅ has_and_belongs_to_many is the no-model many-to-many
Practice quiz
Which association declares that one Author has many Books?
- belongs_to :books
- has_many :books
- has_one :books
- habtm :books
Answer: has_many :books. has_many :books goes in the Author model to express the one-to-many side.
On which model does the foreign key column normally live?
- The model that declares has_many
- Neither model — it is in a join file
- The model that declares belongs_to
- Always the Author model
Answer: The model that declares belongs_to. The belongs_to side holds the foreign key, e.g. books.author_id.
Following Rails conventions, what is the foreign key column for Book belongs_to :author?
- books_author
- author_key
- id_author
- author_id
Answer: author_id. Rails expects a column named author_id by convention.
What does dependent: :destroy do on has_many?
- Prevents deleting the parent
- Deletes associated child records when the parent is destroyed
- Marks children as archived
- Renames the foreign key
Answer: Deletes associated child records when the parent is destroyed. dependent: :destroy removes the children (running their callbacks) when the parent is destroyed.
Which association is used for a many-to-many relationship WITH a join model you can add attributes to?
- has_one
- belongs_to
- has_many :through
- has_and_belongs_to_many
Answer: has_many :through. has_many :through uses an explicit join model, so the join can carry its own columns.
has_and_belongs_to_many (HABTM) requires what in the database?
- A join table with no model
- A single foreign key
- Two separate tables only
- A polymorphic column
Answer: A join table with no model. HABTM uses a join table (e.g. authors_books) with no dedicated model class.
For a Comment that belongs to a Post, what does post.comments return?
- A single Comment
- An ActiveRecord relation of that post's comments
- The post's id
- nil unless cached
Answer: An ActiveRecord relation of that post's comments. has_many gives a collection (relation) of the associated records.
Which line belongs in the Book model for a one-to-many with Author?
- has_many :author
- has_one :author_id
- references :author
- belongs_to :author
Answer: belongs_to :author. The child (Book) declares belongs_to :author.
In a has_many :through, what must the through model itself declare?
- Only validations
- has_many to both sides
- Nothing special
- belongs_to to BOTH joined models
Answer: belongs_to to BOTH joined models. The join model uses belongs_to to each of the two models it connects.
What does inverse_of help Rails do?
- Reverse a migration
- Recognise the two sides of an association point at the same in-memory object
- Delete records faster
- Validate uniqueness
Answer: Recognise the two sides of an association point at the same in-memory object. inverse_of tells Rails the two association ends are the same object, avoiding redundant queries.
Continue this course
- Previous: ActiveRecord Models & Migrations
- Next: Validations & Callbacks — Validate presence/uniqueness/format, errors and lifecycle callbacks
- Quick reference: Ruby cheat sheet
Frequently asked questions
Which side gets has_many and which gets belongs_to?
The side that owns or contains many records gets has_many; the side that holds the foreign key gets belongs_to. In a blog, an Author has_many :books and a Book belongs_to :author, because the books table carries the author_id column. A handy rule of thumb: belongs_to goes on the table that has the _id column. The has_many side gives you a collection (author.books), while the belongs_to side gives you a single related object (book.author).
When should I use has_many :through versus has_and_belongs_to_many?
Use has_many :through when the relationship itself needs to store data or behaviour — for example an Appointment that joins a Doctor and Patient and records the appointment date. The join model is a real class with its own columns, validations, and callbacks. Use has_and_belongs_to_many (HABTM) only for a plain many-to-many with nothing extra to store; it relies on a simple join table (like authors_books) and has no model class. Most experienced Rails developers reach for has_many :through by default because requirements tend to grow.
What does dependent: :destroy actually do?
dependent: :destroy tells Rails that when you destroy a parent record, it should also destroy each associated child record — running their callbacks and their own dependent options along the way. Without it, deleting an author could leave orphaned books pointing at a missing author_id, or raise a foreign-key error. Related options include dependent: :delete_all (faster, but skips child callbacks) and dependent: :nullify (sets the foreign key to NULL instead of deleting). Choose :destroy when child records should never outlive their parent.
Where do foreign keys come from in Rails?
Foreign keys are plain database columns named by convention as <singular_association>_id, such as author_id on the books table. You add them in a migration, usually with t.references :author, foreign_key: true, which creates the author_id column and an index. The belongs_to :author declaration in the model then knows to read and write that column. The foreign_key: true part also adds a real database-level constraint so the database itself refuses rows that point at a non-existent author.