Real domains are relational, and N+1 queries are the most common Rails performance problem.
Associations declare relationships and generate methods:
belongs_to :author, class_name: "User"on the model holding the foreign key (author_id). Since Rails 5 it is required by default; useoptional: trueotherwise.has_many :comments, dependent: :destroyon the other side.dependent:decides what happens to children when the parent is destroyed.has_one :profile.has_many :tags, through: :taggingsfor many-to-many via a join model, which can carry its own columns.has_and_belongs_to_manyexists but a join model is more flexible.- Polymorphic:
belongs_to :commentable, polymorphic: true.
Generated methods include article.comments (a relation you can chain: article.comments.where(approved: true)), article.comments.create!(...) (sets the foreign key), comment.article, article.tag_ids = [1, 2]. counter_cache: true on belongs_to keeps a comments_count column up to date.
The N+1 problem: listing 50 articles and calling article.author.name in the view runs 51 queries. Fix it by eager loading:
includes(:author, comments: :user)lets Rails choose between separate queries and a JOIN.preloadalways uses separate queries;eager_loadalways uses a LEFT OUTER JOIN (needed when you filter on the association).joins(:comments)is for filtering only and doesn't load the association.
Turn on strict_loading (per model, per query or app-wide) to raise on lazy loads during development.
Example
class User < ApplicationRecord
has_many :articles, foreign_key: :author_id, inverse_of: :author
end
class Article < ApplicationRecord
belongs_to :author, class_name: "User"
has_many :comments, dependent: :destroy
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
end
class Comment < ApplicationRecord
belongs_to :article, counter_cache: true # needs articles.comments_count
belongs_to :user
end
# N+1: 1 query for articles + 1 per article for its author
Article.limit(50).each { |a| puts a.author.name }
# Eager loaded: 3 queries total
Article.includes(:author, :tags).limit(50).each do |a|
puts "#{a.title} by #{a.author.name} [#{a.tags.map(&:name).join(', ')}] #{a.comments_count}"
end
Article.joins(:tags).where(tags: { name: "rails" }).distinct # filter only
Article.strict_loading.first.author # raises ActiveRecord::StrictLoadingViolationErrorRemember
- belongs_to holds the foreign key and is required by default
- has_many :through models many-to-many with a real join model
- includes/preload/eager_load fix N+1; joins only filters
Watch out for
- Calling association methods inside a loop over records loaded without includes.