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

Django (models.py)
class Author(models.Model): name = models.CharField(max_length=100)
SQLAlchemy (models.py)
class Author(Base): __tablename__ = "authors" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str]
ActiveRecord (app/models/author.rb)
class Author < ApplicationRecord end
Eloquent (app/Models/Author.php)
class Author extends Model { }
Prisma (schema.prisma)
model Author { id Int @id @default(autoincrement()) name String }
Two files genuinely have an empty class body, and that's not a mistake
ActiveRecord's and Eloquent's own examples above declare zero fields — per Rails' own documentation, "each column in the table is mapped to attributes of the Book class" automatically, at runtime, by inspecting the real database schema directly. Both frameworks trade Django's and SQLAlchemy's own explicit field declarations for real schema introspection: the table itself becomes the single source of truth for what attributes a model has, with the Ruby/PHP class existing mainly to declare behavior, not shape. Prisma goes the opposite direction entirely — its own model isn't a class in the host language at all, but a declarative block in a separate schema file, discussed in full below.

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.

ActiveRecord — Rails' own documentation, quoted directly, including the real generated SQL
# naive: "executes 1 (to find 10 books) + 10 (one per each book to load # the author) = 11 queries in total" books = Book.limit(10) books.each { |book| puts book.author.last_name } # fixed: "this revised approach executes only 2 queries instead of 11" books = Book.includes(:author).limit(10) books.each { |book| puts book.author.last_name } # SELECT books.* FROM books LIMIT 10 # SELECT authors.* FROM authors WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)
Rails' own documentation, quoted directly
"With 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.
Eloquent (Laravel) — Laravel's own documentation, quoted directly
// naive: "25 books, the code above would run 26 queries: one for the // original book, and 25 additional queries" $books = Book::all(); foreach ($books as $book) { echo $book->author->name; } // fixed: only 2 queries, regardless of book count $books = Book::with('author')->get();
Laravel's own documentation, quoted directly
"When accessing Eloquent relationships as properties, the related models are 'lazy loaded'… Eager loading alleviates the 'N + 1' query problem." Eloquent's own real worked example — 25 books, 26 queries — and Rails' own real worked example — 10 books, 11 queries — are the exact same shaped bug Chapter 6 measured directly (20 posts, 21 queries), confirmed in three real, independently-built ORMs.
Prisma — verified real Prisma Client syntax
const authors = await prisma.author.findMany({ include: { posts: true }, });
SystemEager-load syntaxReal, quoted or measured numbers
This site's own Chapter 6Hand-built, batched IN query20 posts: 21 → 2 queries, measured directly
Djangoprefetch_related() / select_related()Django's own docs: "one query for pizzas, one query for all related toppings"
SQLAlchemyselectinload() / joinedload()SQLAlchemy's own docs name the bug "the N plus one problem" directly
ActiveRecordModel.includes(:relation)Rails' own docs: 10 books, 11 → 2 queries, real SQL shown
EloquentModel::with('relation')Laravel's own docs: 25 books, 26 → 2 queries
Prismainclude: { 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:

Rails' own documentation, quoted directly
"Active Record in Rails is an implementation of that pattern" — defined, in the same real documentation, as "an object that wraps a row in a database table, encapsulates the database access, and adds domain logic to that data." Laravel's own docs describe Eloquent in the identical spirit — for years, in Laravel's own words across multiple documented releases: "Eloquent, included with Laravel, provides a beautiful, simple ActiveRecord implementation for working with your database." (Newer Laravel documentation has since reworded its own introduction, but the real mechanics haven't changed — 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:

Prisma's own documentation, quoted directly
"Prisma Client is an auto-generated and type-safe query builder that's tailored to your data." There is no 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.
Neither Active Record nor Data Mapper, verified as its own real category
Active Record ties data and persistence logic to one self-saving class; Data Mapper keeps a hand-written class "entirely separate" from a distinct persistence layer, per Chapter 6's own SQLAlchemy quote. Prisma's real client is neither — there's no hand-written class on either side of the relationship at all, only a declarative schema file and generated code produced from it. Checking a fourth real system directly here, rather than assuming only two patterns exist, is exactly why this chapter's own comparison is worth doing chapter by chapter rather than taking Chapter 6's own two-pattern framing as the complete real picture.

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.

SystemMigration mechanism
DjangoBuilt in — makemigrations / migrate, tracked in the database itself
ActiveRecordBuilt in — per Rails' own docs, tracked via a real schema_migrations table
EloquentBuilt in — real migration files run via Laravel's own Artisan CLI
PrismaBuilt in — prisma migrate dev / prisma migrate deploy, generated from schema.prisma directly
SQLAlchemyNot built in at all — see below
Verified directly — SQLAlchemy itself ships with zero migration tooling
Real, verified fact: Alembic, the tool actually used to manage SQLAlchemy schema changes, is "a database migrations tool written by the author of SQLAlchemy" — a genuinely separate package, installed and configured independently, with its own 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

SystemArchitectural patternField source
DjangoActive RecordExplicit class attributes
SQLAlchemyData MapperExplicit class attributes, mapped separately
ActiveRecordActive Record (the pattern's own namesake)Inferred from the real database schema at runtime
EloquentActive Record, per Laravel's own historical docsInferred from the real database schema at runtime
PrismaNeither — a generated, type-safe clientA 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

Exercise 1

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 solution
Exercise 2

Using 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 solution
Exercise 3

Using 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 solution

Chapter 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