Skip to content

Ruby on Rails Associations — belongs_to has_many has_one and has_many_through Explained

DodaTech Updated 2026-06-28 8 min read

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