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 generate migration AddTitleIndexToPages rails db:migrate rails console

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

# app/helpers/pages_helper.rb — as written in Chapter 4 def breadcrumb_for(page) ancestors = [] current = page while current ancestors.unshift(current) current = current.parent # one query per level, every time end ancestors end

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

@page = Page.includes(parent: { parent: { parent: { parent: :parent } } }).find(params[:id])
Verified: a genuinely different mechanism from both siblings
Eloquent's 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

Not a Rails-specific shortcoming
The nested 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 strategyAlways a single JOINAlways two batched queriesTwo batched queries by default
Can it switch to a JOIN?N/A — always a JOINNoYes — when the association is referenced in a filter
Fixed-depth limitationYes — same nested-chain syntaxYes — same nested-chain syntaxYes — same nested-hash syntax

Hands-On Exercises

Exercise 1

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.

📄 View solution
Exercise 2

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.

📄 View solution
Exercise 3

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.

📄 View solution

Chapter 6 Quick Reference

  • rails console — a real Ruby REPL with every model loaded, fastest way to reproduce a query pattern
  • N+1 caughtbreadcrumb_for's current.parent loop, one query per tree level
  • .includes(parent: { parent: ... }) — nested eager-loading, fixed-depth by necessity
  • Verified nuanceincludes() dynamically chooses two-query vs. single-JOIN, unlike Eloquent's always-two-query with() or Django's always-one-JOIN select_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