Database with Prisma

Website Rebuild with Next.js

Chapter 6 · Database with Prisma

Chapter 4's own getBreadcrumb was planted deliberately — one database query per ancestor level. This chapter catches it, resolved via Prisma's own relational include.

Prisma's Relational include

const pageWithAncestors = await prisma.page.findUnique({ where: { id: pageId }, include: { parent: { include: { parent: true, }, }, }, });

One query, resolved by Prisma into a single nested object — pageWithAncestors.parent.parent — rather than Chapter 4's own repeated single-row query loop.

Fixing the Breadcrumb

// components/PageShell.tsx — updated from Chapter 4 import { prisma } from '@/lib/prisma'; import type { Page } from '@prisma/client'; type PageWithAncestors = Page & { parent: PageWithAncestors | null }; function flattenAncestors(page: PageWithAncestors): Page[] { const chain: Page[] = []; let current: PageWithAncestors | null = page; while (current) { chain.unshift(current); current = current.parent; } return chain; } export default async function PageShell({ page }: { page: Page }) { const pageWithAncestors = await prisma.page.findUniqueOrThrow({ where: { id: page.id }, include: { parent: { include: { parent: true } } }, }); const breadcrumb = flattenAncestors(pageWithAncestors); // ...rest unchanged from Chapter 4 — page prop still used for title/body below }
The loop didn't disappear — it just stopped querying
flattenAncestors still walks a chain with a while loop, one link at a time — but it's walking an object already fully loaded in memory from Prisma's single query, not issuing a new database round-trip per step the way Chapter 4's own version did. The fix isn't "avoid loops," it's "avoid a database query inside the loop."

Verified Honestly Against Every Sibling ORM

Genuine kinship, confirmed from the other direction
Astro Rebuild's own Chapter 6, generated after this course's original chapter set existed, already noted the connection directly: "Drizzle's own relational query API — the with keyword, the nested-object shape — deliberately mirrors Prisma's own include, both in naming and in structure... a real, stated design choice from Drizzle's own documentation." This chapter's own include syntax is the shape every later sibling either mirrors closely (Drizzle) or reaches independently by a different route (Django's select_related, Eloquent's with(), ActiveRecord's includes()) — not a coincidence, and not one-directional flattery either; genuinely convergent design.

The Fixed-Depth Limitation

A real, honest limit — not a Prisma-specific shortcoming
The nested include above only reaches three levels deep — no more. There's no way to express "load the whole ancestor chain, however deep it is" using Prisma's own nested include syntax natively; a page stored deeper than the hardcoded nesting simply has its own remaining ancestors silently absent from the result, with parent: null at the cutoff rather than an error. This isn't a Prisma weakness specifically — it's a structural limit every ORM used across this entire series shares, each one independently rediscovering it in its own chapter, since none of them can express unbounded self-referencing depth without a raw recursive SQL query.

Five ORMs' Eager Loading, Compared

FrameworkSyntax
Next.js (Prisma)include: { parent: { include: { parent: true } } }
Djangoselect_related('parent__parent__parent')
Laravel (Eloquent)with('parent.parent.parent')
Rails (ActiveRecord)includes(parent: { parent: :parent })
Astro (Drizzle)with: { parent: { with: { parent: true } } }

Hands-On Exercises

Exercise 1

Replace PageShell's own getBreadcrumb loop with the include-based query and flattenAncestors() from this chapter, and confirm the same correct, correctly-ordered ancestor chain still renders for a real nested page.

📄 View solution
Exercise 2

Query a top-level page (one with no parent at all) through this chapter's own include clause, and explain what pageWithAncestors.parent equals, and why Prisma doesn't raise an error even though the include asks for a relation that doesn't exist for this row.

📄 View solution
Exercise 3

Store a page five levels deep and confirm the nested include clause from this chapter (three levels) fails to load the full ancestor chain — demonstrating the shared fixed-depth limitation directly, with a real example rather than just accepting the claim.

📄 View solution

Chapter 6 Quick Reference

  • include: { parent: { include: { parent: true } } } — replaces the deliberately-planted N+1 loop with one query
  • flattenAncestors() — still a loop, but walking an already-loaded object in memory, not issuing new queries
  • Verified kinship — Drizzle's own with deliberately mirrors this chapter's include, a documented design goal on Drizzle's own side
  • Fixed-depth limitation — no ORM in this series can express unbounded self-referencing depth natively; every one needs a raw recursive SQL query for that
  • Next chapter: Rendering Content & the Kanji Edge Case