The Database with Django's ORM
Website Rebuild with Django
Chapter 6 · The Database with Django's ORM
The Page model has existed as Python since Chapter 2 — nothing about it is real in an actual database yet. This chapter makes it real, the same job Website Rebuild with Next.js 6 gave to Prisma, and a second real-world instance of relational modeling this site already worked through once, in Food Tracker (Django) 2. Along the way, this chapter also covers the single most common way a Django ORM query quietly costs far more than it looks like it should — and points directly at a real, live example already sitting in this course's own code.
Migrations: makemigrations & migrate
makemigrations compares models.py against what Django last knew about and writes a migration file describing the difference. migrate actually applies it to the database. Prisma's own workflow (prisma migrate dev) does conceptually the same two-step job, but the artifact it produces is different: Prisma generates a raw .sql migration file directly, where Django generates real, executable Python describing the change.
What a Migration File Actually Contains
Even the self-referential parent field appears exactly as declared back in Chapter 2 — Django resolved 'self' down to a real, explicit reference (to='content.page') once it generated this file.
The Django Shell: Testing the ORM Interactively
Prisma Studio does the same job through a GUI; the Django shell is a plain Python REPL with the ORM already imported and ready — no separate tool to install or launch.
QuerySets Are Lazy
A QuerySet only describes a query — it doesn't execute anything until it's actually evaluated (iterated, converted with list(), sliced for display, and so on). Chaining .filter()/.exclude() across several lines never means several queries; it builds up one query description that runs exactly once, at the moment something finally needs real rows.
The N+1 Problem — Including One Already Sitting in This Codebase
node.parent access that hasn't already been loaded triggers its own fresh query — a five-level-deep page means up to five separate round trips just to build one breadcrumb. That's a genuine N+1 pattern, and it's worth naming rather than pretending the code written so far is already perfectly optimized. It's also, honestly, a small and currently acceptable cost: breadcrumb depth is bounded (this site never goes deeper than a handful of levels), so five small queries per page view isn't the kind of problem that demands an immediate fix. The identical pattern applied to something unbounded — a page listing showing many rows, each needing its own parent's title — is where this stops being a minor inefficiency and becomes a real one.
select_related: Fixing It With a Real JOIN
.parent.parent is secretly N separate queries, rather than one, is a genuine, core ORM skill, not a rare edge case. This project's own Chapter 4 breadcrumb code is presented here honestly, as a live example of exactly that pattern, currently small enough not to matter — the same habit of noticing it now is what prevents it from quietly becoming a real problem once this project grows.
Hands-On Exercises
Explain why qs = Page.objects.filter(...).exclude(...), written across two separate lines, doesn't run two separate database queries. What actually triggers the query to run?
Explain why Chapter 4's breadcrumb-building loop is a real N+1 query pattern, and why this chapter treats it as an acceptable, bounded cost right now rather than something demanding an immediate fix.
📄 View solutionGiven a page listing that loops over many pages and accesses page.parent.title for each one, write the corrected queryset using select_related, and explain specifically what problem it solves.
Chapter 6 Quick Reference
makemigrations/migrate— generate a real Python migration file describing model changes, then apply it- Django migrations are Python, not raw SQL — a genuine mechanical difference from Prisma's own
.sqlmigration files python manage.py shell— an interactive Python REPL with the ORM ready to go, Django's own answer to Prisma Studio- QuerySets are lazy — chaining
.filter()/.exclude()builds one query description; nothing runs until it's actually evaluated - The N+1 problem — an unguarded relationship access inside a loop triggers one query per row, not one query total
- A real, current example — Chapter 4's own breadcrumb loop, honestly named as N+1, currently small enough to be acceptable
select_related('field')— fetches a related row via a real SQL JOIN up front, turning N+1 queries into one- Next chapter: Rendering Content & the Kanji Edge Case