Active Record Associations

Ruby on Rails Fundamentals — Active Record Associations
Ruby on Rails Fundamentals
Course 1 · Chapter 6 · Active Record Associations

🔗 Active Record Associations

Chapter 5's Comment migration added a plain post_id integer column — just a number, as far as the database is concerned. This chapter turns that raw foreign key into a real, navigable relationship, replacing the manual JOIN queries Express + mysql2 would otherwise require.

👉 belongs_to

# app/models/comment.rb class Comment < ApplicationRecord belongs_to :post end comment = Comment.find(1) comment.post # => the Post this comment belongs to, one method call comment.post.title # chain straight through to the post's own attributes

belongs_to :post relies on exactly the post_id column Chapter 5's migration created — Rails expects a foreign key named after the singular association (post_id for belongs_to :post) by convention, and generates a comment.post method that looks that row up automatically.

👈 has_many

# app/models/post.rb class Post < ApplicationRecord has_many :comments end post = Post.find(1) post.comments # all comments where post_id = post.id post.comments.where(approved: true) # chainable, exactly like Chapter 5's query methods post.comments.create(body: "Nice post!") # creates a new comment with post_id already set

post.comments replaces manually writing SELECT * FROM comments WHERE post_id = ? against a mysql2 connection. It's also fully chainable — post.comments.where(...) scopes the query further, and post.comments.create(...) automatically sets post_id on the new row without you passing it explicitly.

🔀 has_many :through — Many-to-Many

Tagging a post with multiple tags (and a tag applying to multiple posts) is a classic many-to-many relationship, requiring a join table:

# app/models/post.rb class Post < ApplicationRecord has_many :taggings has_many :tags, through: :taggings end # app/models/tagging.rb -- the join model, sitting between posts and tags class Tagging < ApplicationRecord belongs_to :post belongs_to :tag end # app/models/tag.rb class Tag < ApplicationRecord has_many :taggings has_many :posts, through: :taggings end post.tags # => all tags on this post, joining through taggings automatically

post.tags silently performs the three-table join a hand-written SQL query would need — posts JOIN taggings JOIN tags — behind one method call. This is the association type worth taking slowest: it requires an actual join model (Tagging) with its own belongs_to declarations on both sides, not just a single line.

🗑️ dependent: :destroy

class Post < ApplicationRecord has_many :comments, dependent: :destroy end post.destroy # automatically destroys every associated comment first, no orphaned rows left behind

Without dependent: :destroy, deleting a post would leave its comments in the database with a post_id pointing at nothing — the same orphaned-row problem raw SQL requires either a manual cleanup query or a database-level ON DELETE CASCADE constraint to prevent.

⚠️ The N+1 Query Problem

# Looks innocent -- silently issues 1 + N queries Post.all.each do |post| puts "#{post.title}: #{post.comments.count} comments" end
⚠ This runs one query to get the posts, then one MORE query per post

With 50 posts, the loop above issues 51 separate queries: one for Post.all, then one more SELECT COUNT(*) FROM comments WHERE post_id = ? for every single post inside the loop — the notorious N+1 query problem. This is a genuine reversal of this course's usual pattern: writing the equivalent raw SQL JOIN query by hand in mysql2 forces you to think about efficiency upfront, while Active Record's convenient post.comments syntax can hide this exact performance problem until it's slow in production. The fix is .includes:

# Fixed -- 2 queries total, not 51 Post.includes(:comments).each do |post| puts "#{post.title}: #{post.comments.count} comments" end

.includes(:comments) eager-loads every post's comments in a second, separate query before the loop runs, instead of issuing a fresh query on every iteration — bringing the total back down to two queries regardless of how many posts exist.

📜 Relationships, Side by Side

Manual Joins vs Active Record Associations

Concernmysql2 (Express)Active Record (Rails)
One-to-manySELECT * FROM comments WHERE post_id = ?post.comments
Many-to-manyManual 3-table JOINhas_many :tags, through: :taggings
Cascading deleteManual cleanup query or DB constraintdependent: :destroy
Query efficiencyExplicit — you write exactly the JOIN you needImplicit — easy to accidentally trigger N+1 queries

💻 Coding Challenges

Challenge 1: Wire Up Post and Comment

Add the correct association declarations to Post and Comment (from Chapter 5's migration) so that @post.comments and @comment.post both work. Write one line demonstrating each direction.

Goal: Practice the paired belongs_to/has_many declarations that make a foreign key column into a real, navigable relationship.

→ Solution

Challenge 2: has_many :through for Tags

Following this chapter's Post/Tag/Tagging example, write out the three model files in full (Post, Tag, and the Tagging join model), then write one line showing how you'd get all tags for a given post.

Goal: Get comfortable with the three-model shape a has_many :through relationship actually requires.

→ Solution

Challenge 3: Fix an N+1 Query

Given Author.all.each { |author| puts author.books.count }, explain how many total queries this issues for 20 authors, then rewrite it using .includes to fix the problem, and state the new total query count.

Goal: Recognize the N+1 pattern on sight and know the one-word fix.

→ Solution

💎 Convenient Syntax Doesn't Mean Free

The N+1 problem is this chapter's clearest reminder that post.comments being one short method call doesn't mean it's one cheap operation — it's still a real database query every time it runs, exactly like the hand-written SQL it replaces. Active Record hides the query, not the cost of running it. Reading generated SQL logs (Rails prints every query it runs in development) is a genuinely useful habit for catching this before it reaches production.

🎯 What's Next

Next chapter: Active Record Validations & Callbacksvalidates, before_save, and where Rails expects business rules to actually live inside a model.