The Database: ActiveRecord ORM
Website Rebuild with Ruby on Rails
Chapter 6 · The Database: ActiveRecord ORM
Migrations were already introduced in Chapter 2. This chapter's own real work is catching Chapter 4's breadcrumb_for helper in the act — the same N+1 pattern deliberately planted, and deliberately caught, in every sibling course's own database chapter.
Migrations & the Rails Console, Briefly
rails console drops into a real Ruby REPL with every model already loaded — the fastest way to reproduce the breadcrumb's own query pattern directly and see it happen.
Catching the N+1 Pattern, Live
Loading the capstone's own five-level-deep page triggers five separate SELECT queries just to walk up to the root — exactly the pattern this chapter exists to fix.
.includes(): A Mechanism That Chooses Its Own Strategy
with() always runs two separate, batched queries — one for the base rows, one WHERE IN query for everything related. Django's select_related always runs a single JOIN — one query, full stop. ActiveRecord's includes() is the only one of the three that dynamically chooses: by default it behaves like Eloquent's own two-query strategy, but if the association is also referenced inside a where clause (via .references(:parent)), Rails switches automatically to a single LEFT OUTER JOIN instead. The strategy is a property of how the query is used, not a fixed property of the method itself.
The Same Depth Limitation, Shared Honestly
includes(parent: { parent: ... }) call above pre-loads exactly five levels — no more. A page six levels deep would still trigger one extra query past what was pre-loaded. This is the identical limitation Eloquent's own nested with('parent.parent.parent.parent') and Django's own select_related('parent__parent__parent__parent') already share: none of the three ORMs' built-in eager-loading mechanisms natively express "load the entire ancestor chain, however deep it happens to be." A truly unbounded-depth tree would need a raw recursive SQL query (WITH RECURSIVE) via find_by_sql, genuinely out of scope for this course's own realistic, bounded page hierarchy.
Three ORMs' Eager-Loading, Compared
Django (select_related) | Laravel (with()) | Rails (includes()) | |
|---|---|---|---|
| Default strategy | Always a single JOIN | Always two batched queries | Two batched queries by default |
| Can it switch to a JOIN? | N/A — always a JOIN | No | Yes — when the association is referenced in a filter |
| Fixed-depth limitation | Yes — same nested-chain syntax | Yes — same nested-chain syntax | Yes — same nested-hash syntax |
Hands-On Exercises
Explain why breadcrumb_for triggers exactly five queries for the capstone's own five-level-deep page, and identify which line of the method is responsible.
Explain the real, verified difference between ActiveRecord's includes() and both Eloquent's with() and Django's select_related — specifically, what makes includes()'s own behavior dynamic rather than fixed.
Explain why a nested includes(parent: { parent: ... }) call has a real, honest limitation for an arbitrary-depth tree, and name the two sibling ORMs that share the identical limitation.
Chapter 6 Quick Reference
rails console— a real Ruby REPL with every model loaded, fastest way to reproduce a query pattern- N+1 caught —
breadcrumb_for'scurrent.parentloop, one query per tree level .includes(parent: { parent: ... })— nested eager-loading, fixed-depth by necessity- Verified nuance —
includes()dynamically chooses two-query vs. single-JOIN, unlike Eloquent's always-two-querywith()or Django's always-one-JOINselect_related - Shared honesty — all three ORMs' eager-loading is fixed-depth; a truly unbounded tree needs a raw recursive query
- Next chapter: Rendering Content & the Kanji Edge Case