Ruby on Rails Associations — belongs_to has_many has_one and has_many_through Explained
In this tutorial, you will learn about Ruby on Rails Associations. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby on Rails Associations define relationships between Active Record models using belongs_to, has_many, has_one, and has_many :through, enabling efficient data access with eager loading and automatic JOINs.
What You'll Learn
- Defining the four association types
- Querying through associations
- Using has_many :through for indirect relationships
- Configuring options and callbacks
Why It Matters
Associations are how you model real-world relationships in your data. Doda Browser uses associations for bookmarks-to-folders and history-to-users. Durga Antivirus Pro uses associations for scan-results-to-threats and users-to-licenses.
Real-World Use
E-commerce: User has many Orders, Order belongs to User, Order has many Products through OrderItems. Social: User has many Posts, Post has many Comments, Comment belongs to User.
flowchart LR
A["Associations"] --> B["belongs_to"]
B --> C["has_many"]
C --> D["has_one"]
D --> E["has_many :through"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
belongs_to
The belongs_to association sets up a one-to-one connection with another model. The declaring model's table has the foreign key:
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :article
belongs_to :user
end
# app/models/article.rb
class Article < ApplicationRecord
belongs_to :author, class_name: "User"
end
belongs_to Options
class Comment < ApplicationRecord
# Standard
belongs_to :article
# Optional — allows nil foreign key
belongs_to :reviewer, class_name: "User", optional: true
# Counter cache — stores comments_count on article
belongs_to :article, counter_cache: true
# Touch — updates article's updated_at
belongs_to :article, touch: true
# With foreign key specified
belongs_to :author, class_name: "User", foreign_key: :author_id
# Default scope on association
belongs_to :article, -> { where(published: true) }
# Polymorphic
belongs_to :commentable, polymorphic: true
end
has_many
The has_many association creates a one-to-many relationship:
# app/models/user.rb
class User < ApplicationRecord
has_many :articles
has_many :comments
has_many :bookmarks
end
# app/models/article.rb
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
end
has_many Options
class User < ApplicationRecord
# Standard
has_many :articles
# Dependent behavior
has_many :comments, dependent: :destroy # Delete comments
has_many :sessions, dependent: :delete_all # Fast delete (no callbacks)
has_many :logs, dependent: :nullify # Set user_id to nil
# Custom scope
has_many :published_articles, -> { where(published: true) },
class_name: "Article"
# Through association
has_many :categories, through: :articles
# Source (for through with different name)
has_many :favorited_articles, through: :favorites, source: :article
# With foreign key
has_many :owned_articles, class_name: "Article", foreign_key: :owner_id
# Inverse
has_many :articles, inverse_of: :author
end
has_one
The has_one association creates a one-to-one relationship:
# app/models/user.rb
class User < ApplicationRecord
has_one :profile
has_one :account_settings
# Through another association
has_one :avatar, through: :profile
# Dependent
has_one :preferences, dependent: :destroy
end
# app/models/profile.rb
class Profile < ApplicationRecord
belongs_to :user
has_one :avatar, dependent: :destroy
end
has_one Options
class User < ApplicationRecord
has_one :profile, dependent: :destroy
# With custom class
has_one :settings, class_name: "AccountSetting"
# Through
has_one :avatar, through: :profile
# Required (Rails 7+)
has_one :profile, required: true
end
has_many :through
The has_many :through association creates an indirect relationship through a join model:
# app/models/user.rb
class User < ApplicationRecord
has_many :articles
has_many :comments
has_many :categories, through: :articles
# For many-to-many with additional data
has_many :memberships
has_many :groups, through: :memberships
end
# app/models/membership.rb
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
# Join model can have extra attributes
validates :role, inclusion: { in: %w[member admin owner] }
end
# app/models/group.rb
class Group < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
end
has_and_belongs_to_many (Simpler Alternative)
For simple many-to-many without a join model:
class Article < ApplicationRecord
has_and_belongs_to_many :tags
end
class Tag < ApplicationRecord
has_and_belongs_to_many :articles
end
Requires a join table articles_tags with article_id and tag_id columns (no primary key).
Querying Through Associations
# Find articles by user
user = User.find(1)
drafts = user.articles.where(published: false)
# Chain through multiple associations
user.categories.where(name: "Ruby")
# Eager load to avoid N+1
users = User.includes(articles: [:comments, :tags])
users.each do |u|
u.articles.each { |a| a.comments.each { |c| puts c.body } }
end
# Join
User.joins(:articles).where(articles: { published: true }).distinct
# Association count
user.articles.size # Uses counter_cache if available
user.articles.count # Always COUNT query
user.articles.length # Loads and counts in memory
Polymorphic Associations
A model can belong to multiple other models through a single association:
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
# app/models/article.rb
class Article < ApplicationRecord
has_many :comments, as: :commentable
end
# app/models/photo.rb
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
The comments table needs commentable_id (integer) and commentable_type (string) columns.
Association Callbacks
class User < ApplicationRecord
has_many :articles, after_add: :notify_new_article,
after_remove: :cleanup_article
private
def notify_new_article(article)
NotificationService.new_article(article)
end
def cleanup_article(article)
# Cleanup when article is removed
end
end
Common Mistakes
1. Missing Foreign Key Index
# Bad — no index on foreign key
add_reference :comments, :article
# Good — indexed
add_reference :comments, :article, foreign_key: true, index: true
2. N+1 Queries on Associations
# Bad — queries database for every article's comments
@articles.each { |a| a.comments.each { |c| puts c.body } }
# Good — eager loads in 2 queries
@articles = Article.includes(:comments)
3. Forgetting dependent: :destroy
# Bad — orphaned comments when article is deleted
has_many :comments
# Good — cascade delete
has_many :comments, dependent: :destroy
4. Using has_and_belongs_to_many When You Need a Model
# Bad — can't add attributes to the join
has_and_belongs_to_many :tags
# Good — when join needs extra data
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
5. Circular Association Dependencies
# Bad — circular dependency on destroy
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class Article < ApplicationRecord
belongs_to :user, touch: true
after_destroy :notify_user
end
Practice Questions
1. What's the difference between belongs_to and has_many?
belongs_to goes on the model with the foreign key. has_many goes on the other side. Comment belongs_to Article; Article has_many Comments.
2. When would you use has_many :through?
When you need a join model with additional attributes (membership role, quantity, timestamps). Use for indirect relationships with rich join data.
3. What does dependent: :destroy do?
When the parent record is destroyed, all associated records are also destroyed with callbacks. Use dependent: :delete_all to skip callbacks for performance.
4. What is polymorphic association?
A model can belong to multiple different models through a single association using type + id columns. Example: Comment belongs to both Article and Photo.
Challenge: Design a social network schema with User, Post, Comment, Like, and Follow models with proper associations and cascading deletes.
Solution
# app/models/user.rb
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :likes, dependent: :destroy
# Follow relationships
has_many :active_follows, class_name: "Follow",
foreign_key: :follower_id, dependent: :destroy
has_many :passive_follows, class_name: "Follow",
foreign_key: :followed_id, dependent: :destroy
has_many :following, through: :active_follows, source: :followed
has_many :followers, through: :passive_follows, source: :follower
# Liked posts
has_many :liked_posts, through: :likes, source: :post
def follow(other_user)
active_follows.create(followed: other_user)
end
def following?(other_user)
following.include?(other_user)
end
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
has_many :comments, dependent: :destroy
has_many :likes, as: :likeable, dependent: :destroy
scope :recent, -> { order(created_at: :desc) }
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :user
belongs_to :post, counter_cache: true
belongs_to :parent, class_name: "Comment", optional: true
has_many :replies, class_name: "Comment", foreign_key: :parent_id,
dependent: :destroy
end
# app/models/like.rb
class Like < ApplicationRecord
belongs_to :user
belongs_to :likeable, polymorphic: true
validates :user_id, uniqueness: {
scope: [:likeable_id, :likeable_type],
message: "can only like once"
}
end
# app/models/follow.rb
class Follow < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, uniqueness: { scope: :followed_id }
end
FAQ
{{< faq question="Should I use has_many :through or has_and_belongs_to_many?" >}} Use has_many :through when you need extra attributes on the join (timestamps, quantity, role). Use has_and_belongs_to_many for simple many-to-many with no extra data. {{< /faq >}}
{{< faq question="What does counter_cache do?" >}}
It caches the count of associated records in a column on the parent. belongs_to :article, counter_cache: true auto-updates articles.comments_count, avoiding COUNT queries.
{{< /faq >}}
{{< faq question="How do I avoid N+1 queries?" >}}
Use includes, preload, or eager_load to load associations in advance. Article.includes(:comments, :user) loads all associated data in 3 queries instead of N+1.
{{< /faq >}}
{{< faq question="What is the difference between dependent: :destroy and dependent: :delete_all?" >}}
:destroy calls destroy on each associated record, running callbacks. :delete_all directly deletes with SQL, skipping callbacks. Use :delete_all for performance-critical mass deletes.
{{< /faq >}}
{{< faq question="Can a model have multiple belongs_to associations?" >}} Yes. A Comment can belong to both a User and an Article. Each requires a foreign key column in the comments table. {{< /faq >}}
Try It Yourself
rails new blog_associations
cd blog_associations
# Generate models
bin/rails generate model User name:string email:string
bin/rails generate model Article title:string body:text user:references
bin/rails generate model Comment body:text user:references article:references
bin/rails db:migrate
# In console
user = User.create!(name: "Alice", email: "alice@test.com")
article = user.articles.create!(title: "My Post", body: "Hello!")
comment = article.comments.create!(user: user, body: "Great post!")
puts user.articles.count # 1
puts article.comments.count # 1
puts comment.user.name # Alice
puts User.joins(:articles).distinct.count # 1
What's Next
Now that you understand associations, learn about Active Record validations for ensuring data integrity.
| Topic | Description | Link |
|---|---|---|
| Ruby Validations | Presence, uniqueness, custom validation | {{< ref "31-validations" >}} |
| Ruby Active Record | ORM, queries, CRUD operations | {{< ref "26-active-record" >}} |
| Python SQLAlchemy | Compare Python's ORM associations | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro