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.