Orm Concepts
By the end of this lesson you'll think about your database as PHP objects, not raw SQL — you'll build a tiny Active Record model, tell Active Record apart from Data Mapper, model relationships, and dodge the N+1 query trap that quietly slows real apps down.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ What an ORM Is — and a Tiny Active Record
An ORM (Object-Relational Mapper) maps each database table to a PHP class , each row to an object , and each column to a property . Instead of writing SQL and unpacking arrays, you write User::find(1) and read $user->get("name") . The most common style is Active Record : the model class itself carries the methods to create, find, and save its rows — data and persistence live together. Here is a tiny but complete one, using an in-memory array in place of a real table so it runs anywhere.
That's the whole CRUD lifecycle — C reate ( create ), R ead ( find / where ), U pdate ( set + save ) — on a model. Swap the array for PDO and you've essentially got Laravel's Eloquent . Notice the static methods live on the class (the table) while get / save live on the object (the row): that split is the heart of Active Record.
2️⃣ Active Record vs Data Mapper
There are two ways to connect objects to rows. In Active Record the model saves itself — $user->save() — so business logic and database logic share one class (this is Eloquent's approach). In Data Mapper the model is a plain entity with no database code, and a separate mapper (or repository ) does the persisting (this is Doctrine's approach). Active Record is quicker to write; Data Mapper keeps your domain objects clean and is favoured for large, complex domains. See both side by side.
An entity is just a plain object representing one thing in your domain (an Article, a User). A repository is the object you ask for entities — $repo->find(1) , $repo->save($article) — so the entity never needs to know SQL exists. The trade-off in one line: Active Record optimises for speed of development , Data Mapper optimises for separation of concerns .
3️⃣ Relationships & the N+1 Problem
Tables relate to each other, and so do your objects. A one-to-many relationship (one User hasMany Posts; each Post belongsTo a User) is the everyday case. A many-to-many (a Post has many Tags, a Tag has many Posts) is stored through a third pivot table linking the two. ORMs let you walk these as properties: $user->posts . But how they fetch matters. Lazy loading fetches a relation only when you touch it; eager loading fetches it up front. Lazy loading inside a loop causes the dreaded N+1 problem — watch the query count.
With two users the lazy version is harmless (3 queries) — but with 100 users it becomes 101 queries, while the eager version stays at 2 no matter what. That's why real ORMs give you eager loading: Eloquent's User::with('posts')->get() or Doctrine's JOIN fetch in DQL. Reach for it the moment you read a relationship inside a loop.
4️⃣ Migrations, Eloquent & Doctrine
A migration is a versioned PHP file that describes a schema change — "create a users table", "add an email column" — so your database structure lives in code and replays on any machine in order. You run them with a command, and the tool records which have already been applied. The two dominant PHP ORMs sit on top of this idea: Eloquent (Laravel) uses Active Record ; Doctrine (Symfony) uses Data Mapper . Here's the same model expressed in each, plus a migration — this snippet is for reading, not running (it needs a full framework).
5️⃣ Your Turn: Finish the Model
Now you try. The Active Record model below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
One more — this time about performance . Make the eager strategy use exactly two queries, no matter how many users there are.
📋 Quick Reference — Active Record vs Data Mapper
No code is filled in this time — just a brief and an outline. Build a Data Mapper-style repository yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is exactly the write-run-check loop you'll use on every real model.
Practice quiz
What does an ORM (Object-Relational Mapper) map?
- URLs to controller methods
- Functions to HTTP routes
- Tables to classes, rows to objects, and columns to properties
- Files to database backups
Answer: Tables to classes, rows to objects, and columns to properties. An ORM maps each table to a class, each row to an object, and each column to a property, so you work with $user->name instead of arrays.
In the Active Record pattern, who is responsible for persisting a model?
- The model itself — e.g. $user->save() writes the object to its table
- A separate mapper or repository class
- The database driver alone
- A migration file
Answer: The model itself — e.g. $user->save() writes the object to its table. In Active Record the model knows how to save and find itself — data and persistence live together (Eloquent's style).
How does the Data Mapper pattern differ from Active Record?
- It stores everything in JSON files
- It does not use classes at all
- It requires the entity to extend the database connection
- The entity is a plain object with no database code, and a separate mapper/repository handles persistence
Answer: The entity is a plain object with no database code, and a separate mapper/repository handles persistence. Data Mapper keeps the entity clean (no SQL); a separate mapper persists it — Doctrine's style, favoured for large domains.
Which PHP ORM uses the Active Record pattern?
- Doctrine
- Eloquent (Laravel)
- PDO
- Flysystem
Answer: Eloquent (Laravel). Eloquent (Laravel) is Active Record; Doctrine (Symfony) is Data Mapper.
What is the N+1 query problem?
- Loading N parent rows with one query, then firing one extra query per parent for its related rows — N+1 total
- Running one query that returns too many rows
- A query that fails after N retries
- Using N+1 database connections at once
Answer: Loading N parent rows with one query, then firing one extra query per parent for its related rows — N+1 total. Loading 100 users then reading each user's posts on demand becomes 101 round-trips — slow and wasteful.
How do you fix the N+1 problem?
- Add more database servers
- Cache the entire database in memory
- Use eager loading to fetch the related rows up front in one extra query, e.g. Eloquent's User::with('posts')->get()
- Switch from PDO to mysqli
Answer: Use eager loading to fetch the related rows up front in one extra query, e.g. Eloquent's User::with('posts')->get(). Eager loading turns 101 queries into 2 by fetching parents and all their related rows up front.
When should you prefer eager loading over lazy loading?
- Always, for every single record
- Whenever you iterate a collection and read a relationship inside the loop
- Only when the database is small
- Never — lazy loading is always faster
Answer: Whenever you iterate a collection and read a relationship inside the loop. Lazy-load single records by default, but eager-load (with(...)) whenever a loop touches a relationship, to avoid N+1.
How is a many-to-many relationship (e.g. Posts and Tags) stored?
- In a single table with comma-separated values
- By duplicating rows in both tables
- It cannot be modelled by an ORM
- Through a third 'pivot' table linking the two
Answer: Through a third 'pivot' table linking the two. A many-to-many relationship is stored through a pivot table that links the two sides.
What is a migration in the context of an ORM?
- Moving the database to a new server
- A versioned PHP file that describes a schema change, so the structure lives in code and replays on any machine in order
- Converting Active Record to Data Mapper
- A backup of all the data
Answer: A versioned PHP file that describes a schema change, so the structure lives in code and replays on any machine in order. Migrations keep schema changes in versioned, replayable code (php artisan migrate / doctrine:migrations:migrate), not hand-edits.
Why is hand-editing the production schema discouraged?
- It is slower than a migration
- PHP forbids direct schema edits
- Teammates and production can't reproduce the change; migrations make schema changes reviewable, shareable, and reversible
- It deletes existing data automatically
Answer: Teammates and production can't reproduce the change; migrations make schema changes reviewable, shareable, and reversible. Hand edits aren't reproducible; every change should go through a migration so others can replay it exactly.