How Active Record Associations Work In Ruby On Rails
Learn how Active Record associations work in Ruby on Rails with practical examples of has_many, belongs_to, and more.
• 15 min read
• 15 min read
Learn how Active Record associations work in Ruby on Rails with practical examples of has_many, belongs_to, and more.
• 15 min read
• 15 min read
Every real Rails application is, at its core, a web of related data. Users write articles. Articles belong to categories. Comments get left on posts. Orders contain line items. None of these things exist in isolation, and if you have ever tried to model a database by hand, you know how quickly the plumbing between tables can get messy. This is exactly the problem Active Record associations were built to solve.
Active Record associations let you describe how your models relate to each other in plain, readable Ruby, and then Rails handles the SQL joins, foreign keys, and object loading behind the scenes. Instead of writing raw queries every time you need a user's articles or an article's comments, you just call a method like user.articles and Rails takes care of the rest.
In this guide, you will learn what associations are, how the major types work, when to reach for each one, and how to avoid the mistakes that trip up even experienced developers. We will build a real blogging application together, using Rails 8 conventions throughout, so you can see these concepts in a context you will actually recognize from production apps. If you are still deciding whether Rails is the right framework to invest your time in, our article on whether Ruby on Rails is still worth learning in 2026 is a good companion read before diving in here.
An Active Record association is a declaration inside a model that describes its relationship to another model. When you write has_many :articles inside a User model, you are telling Rails that a user can have many articles, and Rails automatically generates a set of methods so you can work with that relationship as if it were a natural part of the Ruby object.
Associations exist because relational databases and object-oriented code think about relationships differently. A database sees a user_id column sitting on the articles table. Ruby wants to see user.articles as a collection it can loop through, filter, and manipulate. Associations bridge that gap.
The benefits are significant:
Think of associations like a well-organized filing cabinet. Each drawer (table) holds its own kind of document, but a label system (foreign keys) tells you which documents in one drawer relate to documents in another. Active Record associations are that label system, translated into Ruby.
Before jumping into Rails syntax, it helps to understand the three fundamental types of relationships found in relational databases.
A single record in one table relates to exactly one record in another table.
User ----- Profile
1 1
Example: a user has one profile.
A single record in one table relates to many records in another table.
User ----------- Articles
1 many
Example: a user can write many articles, but each article belongs to one user.
Many records in one table relate to many records in another, usually through a join table.
Users ---- Enrollments ---- Courses
many join many
Example: a user can enroll in many courses, and a course can have many enrolled users.
Rails gives you specific association types for each of these shapes, which is exactly what we will cover next.
belongs_to is used on the model that holds the foreign key. If your articles table has a user_id column, the Article model belongs to User.
# app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
end
This assumes a migration like this:
class CreateArticles < ActiveRecord::Migration[8.0]
def change
create_table :articles do |t|
t.string :title
t.text :body
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
The t.references :user line creates the user_id column and, when paired with foreign_key: true, adds a real foreign key constraint at the database level. This is one of the most important habits to build early: let the database enforce integrity, do not rely on application code alone.
With this in place, you can call:
article.user
and Rails will fetch the associated user with a simple query.
As of Rails 5, belongs_to associations are required by default, meaning a record cannot be saved without an associated parent unless you explicitly opt out. If the relationship is genuinely optional, such as an article that might not yet have an assigned editor, mark it accordingly:
belongs_to :editor, class_name: "User", optional: true
Best practice: always add a database-level foreign key alongside the association, and be explicit about optional: true rather than relying on undocumented behavior.
has_one is the mirror image of belongs_to, used when a model owns exactly one related record, and the foreign key lives on the other table.
# app/models/user.rb
class User < ApplicationRecord
has_one :profile
end
# app/models/profile.rb
class Profile < ApplicationRecord
belongs_to :user
end
Here, the profiles table holds the user_id column, not the other way around. This is a common source of confusion for developers new to Rails: has_one never means the current model holds the foreign key.
A practical example is separating frequently accessed user data from rarely accessed profile details:
user.profile.bio
user.profile.website_url
Common pitfalls with has_one:
has_one record without first saving the parent.has_one prevents duplicate rows. It does not, at the database level, unless you add a unique index on the foreign key column yourself.has_many is the workhorse of Active Record associations and describes a one-to-many relationship from the "one" side.
class User < ApplicationRecord
has_many :articles
end
With this single line, Rails generates a whole family of useful methods:
user.articles # all articles belonging to the user
user.articles.create(title: "New Post")
user.articles.build(title: "Draft")
user.articles.where(published: true)
user.articles.count
user.articles.find(5)
These methods let you treat user.articles almost like an ActiveRecord::Relation, because that is exactly what it is. You can chain scopes, add conditions, and order results just as you would with any other query.
A real-world example: showing a user's published articles on their profile page.
@published_articles = current_user.articles.where(published: true).order(created_at: :desc)
This single line replaces what would otherwise require a manual SQL join and manual object mapping.
Sometimes a many-to-many relationship needs extra data attached to the join itself, or you simply want more control than a basic join table provides. That is where has_many :through comes in.
Consider a User, Course, and Enrollment setup, where an enrollment tracks when a user joined a course and whether they completed it.
class User < ApplicationRecord
has_many :enrollments
has_many :courses, through: :enrollments
end
class Course < ApplicationRecord
has_many :enrollments
has_many :users, through: :enrollments
end
class Enrollment < ApplicationRecord
belongs_to :user
belongs_to :course
end
The migration for the join model looks like a normal table, not a special join table:
class CreateEnrollments < ActiveRecord::Migration[8.0]
def change
create_table :enrollments do |t|
t.references :user, null: false, foreign_key: true
t.references :course, null: false, foreign_key: true
t.datetime :enrolled_at
t.boolean :completed, default: false
t.timestamps
end
end
end
Now you can do things like:
user.courses
user.enrollments.where(completed: true)
course.users.where(enrollments: { completed: true })
The major advantage over a plain many-to-many join table is that Enrollment is a real model. You can validate it, add callbacks to it, and query it directly, which becomes essential the moment your join needs more than just two foreign keys.
Best practice: default to has_many :through for any many-to-many relationship where the connection itself carries meaningful data, which in practice is most of them.
has_and_belongs_to_many, often shortened to HABTM, is Rails' simpler tool for many-to-many relationships. It relies on a join table with no primary key and no corresponding model.
class Article < ApplicationRecord
has_and_belongs_to_many :tags
end
class Tag < ApplicationRecord
has_and_belongs_to_many :articles
end
The migration creates a plain join table:
class CreateArticlesTagsJoinTable < ActiveRecord::Migration[8.0]
def change
create_join_table :articles, :tags do |t|
t.index :article_id
t.index :tag_id
end
end
end
HABTM works fine for simple cases, tagging being the classic example, where the join itself never needs extra columns or independent validation.
Its limitations become obvious quickly. You cannot easily add a created_at timestamp to track when a tag was applied, you cannot validate the join itself, and there is no model to hang business logic on. For this reason, most experienced Rails developers reach for has_many :through by default and treat HABTM as the exception rather than the rule.
Polymorphic associations let a single model belong to more than one other type of model using one association.
A classic example is a Comment model that can belong to either an Article or a Photo.
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Article < ApplicationRecord
has_many :comments, as: :commentable
end
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
The migration adds two columns instead of one: a commentable_id and a commentable_type.
class CreateComments < ActiveRecord::Migration[8.0]
def change
create_table :comments do |t|
t.text :body
t.references :commentable, polymorphic: true, null: false
t.timestamps
end
end
end
commentable_type stores the class name, "Article" or "Photo", while commentable_id stores the record's id. Rails uses both together to figure out exactly which record a comment belongs to.
The benefit is obvious: instead of building a separate comments table for every commentable model, you get one flexible table that can grow to support new commentable types with zero schema changes.
A self join is when a model associates with itself. The textbook example is an employee and manager relationship, where a manager is simply another employee.
class Employee < ApplicationRecord
belongs_to :manager, class_name: "Employee", optional: true
has_many :direct_reports, class_name: "Employee", foreign_key: "manager_id"
end
The migration only needs one extra self-referencing foreign key:
class CreateEmployees < ActiveRecord::Migration[8.0]
def change
create_table :employees do |t|
t.string :name
t.references :manager, foreign_key: { to_table: :employees }
t.timestamps
end
end
end
With this setup:
employee.manager
employee.direct_reports
Rails handles the recursive relationship cleanly, because under the hood it is still just a belongs_to and has_many pair, pointed at the same table using class_name and foreign_key to clarify the intent.
Real applications rarely stop at a single relationship. A blog post belongs to a user, who belongs to a company, and you might need to walk that whole chain.
article.user.company.name
Rails also lets you query nested associations directly:
Article.joins(user: :company).where(companies: { name: "Acme" })
For association loading rather than filtering, you can nest includes to avoid the N+1 problem entirely:
articles = Article.includes(user: :company)
This structure of nested associations scales naturally as your data model grows, but it is also where performance problems tend to creep in if you are not deliberate about eager loading, something we will return to shortly.
Rails gives you several options to fine-tune how associations behave.
dependent: :destroy removes associated records (running their callbacks) when the parent is destroyed.
has_many :articles, dependent: :destroy
dependent: :delete_all removes associated records directly at the database level, skipping callbacks, which is faster but less thorough.
optional: true allows a belongs_to association to be saved without a parent present.
class_name tells Rails the actual class when the association name does not match it directly, as seen earlier with manager.
foreign_key overrides the default foreign key column name Rails expects.
source is used inside has_many :through when the association name on the join model does not match the target association name.
has_many :taggings
has_many :tags, through: :taggings, source: :tag
inverse_of helps Rails recognize both sides of a bidirectional association, which improves consistency and avoids redundant queries in some cases.
touch updates the parent's updated_at timestamp whenever the child is saved.
belongs_to :article, touch: true
counter_cache keeps a cached count of associated records on the parent, avoiding a COUNT query every time you need the number of, say, comments on an article.
belongs_to :article, counter_cache: true
This requires a comments_count integer column on the articles table, which Rails keeps in sync automatically.
Once you start working with associations, you will inevitably encounter the N+1 query problem, where loading a list of records triggers a separate query for each record's associations. Rails gives you three main tools to fix this: includes, preload, and eager_load.
Article.includes(:user).each { |article| puts article.user.name }
Eager loading matters because a page that quietly runs one query per row in a loop can feel fast with ten records and grind to a halt with ten thousand. We cover the differences between includes, preload, and eager_load in much more depth, along with real query plans, in our dedicated guide on how to fix N+1 queries in Rails, which is worth reading right after this article if performance is on your mind.
Associations and validations often work together to protect data integrity.
class Article < ApplicationRecord
belongs_to :user
validates :title, presence: true
end
Because belongs_to is required by default in modern Rails, an article without a user will fail validation automatically. For associations where the parent should validate the presence of its children, you can add that explicitly:
class User < ApplicationRecord
has_many :articles
validates :articles, presence: true, on: :publish_profile
end
When it comes to dependent records, always be intentional about what happens on deletion. Silently orphaning rows, where child records are left pointing at a foreign key that no longer exists, is one of the fastest ways to corrupt a production database. Pair every has_many with a deliberate dependent option rather than leaving Rails to the default, which does nothing at all.
Even experienced developers run into these issues:
t.references in a migration leads to confusing runtime errors instead of a clear database constraint violation.has_many when you meant has_one, or vice versa, usually surfaces as a subtle bug rather than an immediate crash.class_name and clearer naming.A few habits will keep your associations fast as your application grows:
includes, preload, or eager_load) whenever you are looping over a collection and accessing an association.counter_cache for frequently displayed counts instead of running COUNT queries on every page load.t.references, but is worth double-checking on older migrations.where rather than loading everything into Ruby and filtering with select or find.If you are running Rails on SQLite in production, which has become an increasingly popular and genuinely solid option, these performance habits matter even more. Our guide on optimizing SQLite for Rails 8 in production walks through configuration choices that pair well with the association patterns covered here.
Let's tie everything together by sketching out a small blogging platform using the associations we have covered.
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class Category < ApplicationRecord
has_many :articles
end
class Article < ApplicationRecord
belongs_to :user
belongs_to :category, optional: true
has_many :comments, as: :commentable, dependent: :destroy
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
end
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
belongs_to :user
end
class Tag < ApplicationRecord
has_many :taggings
has_many :articles, through: :taggings
end
class Tagging < ApplicationRecord
belongs_to :article
belongs_to :tag
end
Walking through the relationships:
has_many :through join model called Tagging, giving us room to later add fields like featured: true on the join itself.Rendering a page for a single article, with all of its associations eager loaded to avoid N+1 queries, might look like this:
@article = Article.includes(:user, :category, :tags, comments: :user).find(params[:id])
That one line replaces what would otherwise be five or six separate queries scattered across your view, all handled cleanly through associations you defined once.
When should I use belongs_to? Use belongs_to on whichever model's table holds the foreign key. If the articles table has a user_id column, belongs_to :user goes on the Article model.
What is the difference between has_many and has_one? has_many describes a one-to-many relationship where the parent can have multiple associated records. has_one describes a one-to-one relationship where the parent has exactly one associated record. Both keep the foreign key on the other table.
Should I use HABTM or has_many :through? Use has_many :through for most many-to-many relationships, especially any time the join itself might need extra data or validation. Reserve HABTM for very simple, unlikely-to-change join relationships like basic tagging.
What are polymorphic associations? A polymorphic association lets one model belong to more than one other model type through a single association, using a type column alongside the foreign key to track which model it points to.
How do I avoid N+1 queries? Use includes, preload, or eager_load whenever you plan to loop through a collection of records and access an association on each one.
How do associations affect performance? Associations themselves are efficient, but how you use them matters. Loading associations one at a time inside a loop is slow, while eager loading, indexing foreign keys, and using counter caches keep things fast even as your data grows.
Active Record associations turn the tedious work of managing relational data into expressive, readable Ruby. belongs_to and has_one describe ownership from the child and parent side of a relationship. has_many handles the common one-to-many case. has_many :through and HABTM cover many-to-many relationships, with has_many :through being the more flexible and generally preferred choice. Polymorphic associations and self joins solve more specialized modeling problems without forcing awkward schema workarounds.
Mastering associations is one of the clearest paths to writing cleaner, more maintainable Rails applications. The best way to internalize all of this is to build something real. Take a small project, model out the relationships between your data, and practice reaching for the right association type for each shape of relationship you encounter. If you are just getting started with Rails as a whole, revisit our article on whether Ruby on Rails is still worth learning in 2026 for a broader view of where the framework stands today.