Active Record Associations
🔗 Active Record Associations
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
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
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:
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
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
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:
.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
| Concern | mysql2 (Express) | Active Record (Rails) |
|---|---|---|
| One-to-many | SELECT * FROM comments WHERE post_id = ? | post.comments |
| Many-to-many | Manual 3-table JOIN | has_many :tags, through: :taggings |
| Cascading delete | Manual cleanup query or DB constraint | dependent: :destroy |
| Query efficiency | Explicit — you write exactly the JOIN you need | Implicit — 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.
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.
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.
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 & Callbacks — validates, before_save, and where Rails expects business rules to actually live inside a model.