The Database: Eloquent ORM

Website Rebuild with Laravel

Chapter 6 · The Database: Eloquent ORM

The migration and model exist since Chapter 2 — this chapter is about actually living with Eloquent day to day: running the migration for real, Laravel's own interactive shell, and the exact same N+1 query trap Django Rebuild 6 caught, already sitting in this course's own Chapter 4 breadcrumb code too.

Running the Migration & php artisan tinker

php artisan migrate php artisan tinker >>> $root = Page::create(['title' => 'Programming', 'slug' => 'programming']); >>> $child = Page::create(['title' => 'Python', 'slug' => 'python', 'parent_id' => $root->id]); >>> $child->full_path => "programming/python"

tinker is Laravel's own interactive REPL — the direct equivalent of Django's own manage.py shell. Page::create([...]) mass-assigns through Chapter 2's own $fillable list — the same protection is doing real work on every call here, not just a one-time setup detail.

Eloquent's Query Builder: Chained, Then a Real Collection

$pages = Page::where('title', 'like', '%Python%')->orderBy('title')->get(); // ->get() is a TERMINAL method — the query runs immediately, right here // $pages is now a real Eloquent Collection — already fetched, not still queryable

Chained methods like ->where() and ->orderBy() build up the query without running it — ->get() (or ->first(), ->count()) is what actually executes it. What comes back from a terminal method is a Collection — a real, already-fetched, enhanced array-like object with its own useful methods (->map(), ->filter(), ->pluck()), not something you can call ->where() on again to keep refining the same database query.

A Real, Live N+1 Example — Again

// Chapter 4's own Controller breadcrumb loop, revisited $node = $page; while ($node) { array_unshift($breadcrumb, $node); $node = $node->parent; // each access not already loaded triggers its own query }
Honest, not overstated: the same pattern, a third time in this course's own family
Every $node->parent access that hasn't already been loaded triggers its own fresh query — a genuine N+1 pattern, exactly like the one Django Rebuild 6 caught in its own identical breadcrumb code. And exactly the same reasoning applies here too: breadcrumb depth is bounded (this site never goes more than a handful of levels deep), so this stays a small, currently acceptable cost — worth naming honestly rather than pretending the code is already perfectly optimized, not worth an urgent fix on its own.

->with(): Eager Loading, But Not a JOIN

// Without eager loading — N+1, a fresh query every iteration $pages = Page::whereNotNull('parent_id')->get(); foreach ($pages as $page) { echo $page->parent->title; } // With eager loading — exactly TWO queries total, not N+1 $pages = Page::with('parent')->whereNotNull('parent_id')->get(); foreach ($pages as $page) { echo $page->parent->title; // no extra query — already loaded }
A real, verified mechanism difference — not just different syntax
Django's select_related('parent') fetches the related rows in one query, via a genuine SQL JOIN. Eloquent's with('parent') solves the identical N+1 problem, but through a genuinely different mechanism: it runs a second, separate query — a batched WHERE id IN (...) against every parent ID collected from the first query's results — then stitches the two result sets together in PHP memory. Both fully eliminate the N+1 pattern; one does it in a single joined query, the other in two batched ones. Neither is "wrong" — they're just genuinely different SQL underneath an almost identical-looking call.

Hands-On Exercises

Exercise 1

Explain what Page::where(...)->get() actually returns, and why you can't chain another ->where() onto the result afterward the way you might expect from a "queryset" mental model.

📄 View solution
Exercise 2

Explain why the Controller's own breadcrumb loop from Chapter 4 has the identical N+1 pattern Django Rebuild 6 caught in its own breadcrumb code, and why this chapter still treats it as acceptable for now.

📄 View solution
Exercise 3

Explain the real mechanical difference between Django's select_related (one query, a JOIN) and Eloquent's with() (two queries, a batched WHERE IN). Both solve N+1 — how do they actually differ underneath?

📄 View solution

Chapter 6 Quick Reference

  • php artisan tinker — Laravel's own interactive shell, the direct equivalent of Django's manage.py shell
  • Eloquent's query builder — chained methods build the query lazily; a terminal method (->get(), ->first()) executes it immediately and returns a real, already-fetched Collection
  • A live N+1 example, again — Chapter 4's own breadcrumb loop, honestly acceptable for now given its bounded depth
  • with('parent') — Eloquent's eager loading; solves N+1 in exactly two queries total
  • A real mechanism difference: Django's select_related uses one query with a JOIN; Eloquent's with() uses two queries with a batched WHERE IN, not a join
  • Next chapter: Rendering Content & the Kanji Edge Case