Data Access Compared: Django ORM, SQLAlchemy, ActiveRecord, Eloquent & Prisma
Web Framework Internals
Chapter 7 ยท Data Access Compared: Django ORM, SQLAlchemy, ActiveRecord, Eloquent & Prisma
Chapter 6 built one minimal ORM and ran directly into the N+1 problem, the Active Record vs. Data Mapper question, and the real question of where a model's own field mapping actually comes from. This chapter checks all three against five real systems — Django's ORM, SQLAlchemy, Ruby's ActiveRecord (the library the pattern is directly named after), Laravel's Eloquent, and Prisma — plus a topic Chapter 6 never touched at all: how each one manages a database schema changing over time.
The Same Model, Five Real Syntaxes
The Same N+1 Fix, Five Real Answers
Chapter 6 measured 21 real queries for 20 posts collapsing to 2. Checking each system's own documentation directly shows the identical fix, worked out independently five times, with genuinely matching numbers.
includes, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries." The real generated SQL shown above — a second query with a literal WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10) — is the identical batching technique Chapter 6 built by hand, independently rediscovered inside a completely different real ORM.
| System | Eager-load syntax | Real, quoted or measured numbers |
|---|---|---|
| This site's own Chapter 6 | Hand-built, batched IN query | 20 posts: 21 → 2 queries, measured directly |
| Django | prefetch_related() / select_related() | Django's own docs: "one query for pizzas, one query for all related toppings" |
| SQLAlchemy | selectinload() / joinedload() | SQLAlchemy's own docs name the bug "the N plus one problem" directly |
| ActiveRecord | Model.includes(:relation) | Rails' own docs: 10 books, 11 → 2 queries, real SQL shown |
| Eloquent | Model::with('relation') | Laravel's own docs: 25 books, 26 → 2 queries |
| Prisma | include: { relation: true } | One batched query in place of one per parent row |
Active Record, Data Mapper, and a Genuine Third Answer
Chapter 6 named two real architectural patterns from Django's and SQLAlchemy's own documentation. Checking ActiveRecord's own real documentation shows it isn't just an example of the Active Record pattern — it's the library the pattern is named after:
Model::find() and $model->save(), verified directly in this chapter's own examples above, are the same self-saving-instance shape Chapter 6 built by hand.)
Prisma is the real, genuine outlier — not a spelling variant of either pattern, but a third real answer entirely, verified directly against its own documentation:
Author class written by hand anywhere in a real Prisma project — the schema.prisma block shown earlier in this chapter is read by a real, separate prisma generate command, which "reads your Prisma schema and generates Prisma Client code." The object a developer actually calls .findMany() on is generated source code, regenerated every time the schema changes, not a class either the developer or the framework wrote directly.
Migrations: How Five Real Systems Handle Schema Change
Chapter 6 never asked how a table's own real structure gets created or changed in the first place. Checking all five systems' own documentation directly turns up a genuine, load-bearing gap in one of them.
| System | Migration mechanism |
|---|---|
| Django | Built in — makemigrations / migrate, tracked in the database itself |
| ActiveRecord | Built in — per Rails' own docs, tracked via a real schema_migrations table |
| Eloquent | Built in — real migration files run via Laravel's own Artisan CLI |
| Prisma | Built in — prisma migrate dev / prisma migrate deploy, generated from schema.prisma directly |
| SQLAlchemy | Not built in at all — see below |
alembic.ini file and its own versions/ directory. Every other system checked in this chapter bundles migrations directly into the same tool that defines the models. This isn't an oversight in SQLAlchemy — it's the direct, structural consequence of Chapter 6's own Data Mapper finding: a design that deliberately keeps the mapped class "entirely separate" from persistence logic has no natural home inside the ORM itself for a tool that changes the database's own real structure.
Prisma's own real migration workflow is worth a second look, since it's genuinely different in kind from the other three built-in systems: per Prisma's own documentation, Prisma Migrate is "a hybrid database schema migration tool… Declarative: The data model is described in a declarative way in the Prisma schema." A developer edits schema.prisma directly (declaring the desired end state, not a step-by-step change), then runs prisma migrate dev --name <description>, which generates the real, imperative SQL migration file by diffing the new schema against the old one — genuinely different from Django's, Rails', and Laravel's own migration files, which a developer (or a code generator working from their own model changes) writes directly as the real, primary artifact.
One Feature, Five Real Answers
| System | Architectural pattern | Field source |
|---|---|---|
| Django | Active Record | Explicit class attributes |
| SQLAlchemy | Data Mapper | Explicit class attributes, mapped separately |
| ActiveRecord | Active Record (the pattern's own namesake) | Inferred from the real database schema at runtime |
| Eloquent | Active Record, per Laravel's own historical docs | Inferred from the real database schema at runtime |
| Prisma | Neither — a generated, type-safe client | A separate schema.prisma file, compiled into real generated code |
Where This Course Is Headed
Chapter 8 moves to the layer that wraps every request and response passing through a framework at all — middleware, built and verified from scratch the same way this chapter's own ORM concepts were in Chapter 6.
Hands-On Exercises
Using this chapter's own real Prisma quotes, explain why a schema.prisma change requires an explicit prisma generate (or migrate dev) step to take effect, in a way that Django's, ActiveRecord's, and Eloquent's own class-based models never need after editing a model file directly.
๐ View solutionUsing this chapter's own real ActiveRecord example (10 books, 11 queries down to 2, with the real generated SQL shown) as a model, write out what the equivalent two real generated SQL statements would look like for 30 books written by only 4 distinct authors, and explain why the second statement's own IN clause would contain 4 values, not 30.
๐ View solutionUsing this chapter's own quotes about SQLAlchemy's Data Mapper design and Alembic being a separate package, explain why that gap is a structural consequence of the pattern itself rather than an oversight โ and why Django and ActiveRecord, both Active Record systems, never faced the identical design question at all.
๐ View solutionChapter 7 Quick Reference
- Five real model syntaxes โ Django/SQLAlchemy declare fields explicitly; ActiveRecord/Eloquent infer them from the real database schema at runtime; Prisma declares them in a separate schema file entirely
- The same N+1 fix, five real answers โ Rails' own 10 books/11โ2 queries with real generated SQL, Laravel's own 25 books/26โ2 queries, Prisma's include โ all independently verifying Chapter 6's own 21โ2 finding
- A genuine third architectural answer โ Prisma's real generated client is neither Active Record nor Data Mapper; there's no hand-written model class on either side at all
- SQLAlchemy ships with zero built-in migrations โ Alembic is a real, genuinely separate package, a direct structural consequence of the Data Mapper design itself
- Prisma's own migrations are a real hybrid โ a declarative schema.prisma file, diffed to generate the real, imperative SQL migration file automatically
- Next chapter: Middleware & the request/response lifecycle โ how it actually works