ActiveRecord vs DataMapper — two ORM patterns, different trade-offs
Intermediate5 min read·eng-15-002
interviewsqlcompare
Concept
ActiveRecord vs DataMapper — two ORM patterns with fundamentally different designs.
ActiveRecord (pattern + Laravel's implementation name): The model class is BOTH the domain object AND the persistence mechanism. User::find(), $user->save(), $user->delete() — the model knows how to store itself.
- Table = class:
Usermodel →userstable. - Row = instance: Each
Userobject represents one row. - The object knows about the database: It extends
Model, which has all DB operations. - Simple and fast: Few classes, everything in one place. Laravel, Rails, Django ORM use ActiveRecord.
- Downside: Business logic and persistence are coupled. Hard to use the model without a database connection. Fat models emerge naturally.
DataMapper (Doctrine uses this): The model is a PLAIN PHP class (POPO — Plain Old PHP Object) with no DB knowledge. A separate "mapper" or "EntityManager" handles persistence.
- Clean separation: Domain class
Useris justpublic string $name. Noextends Model. - Domain objects are truly portable: Unit test
Userwith no database involved. - More complex: More files (Entity, Repository, Mapper). Steeper learning curve.
- Better for DDD: The domain model doesn't know about persistence — it's a pure domain concept.
- Symfony/Doctrine, DDD-style frameworks use DataMapper.
When to use which:
- ActiveRecord: CRUD apps, rapid development, typical web projects. Laravel/Eloquent.
- DataMapper: Complex domain logic, large teams, DDD approach, need to test domain in isolation.
Code Example
php
<?php
// ============================================================
// ACTIVERECORD — Eloquent style
// ============================================================
class User extends \Illuminate\Database\Eloquent\Model
{
// The object ITSELF knows how to persist
protected $fillable = ['name', 'email'];
public function orders(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Order::class);
}
}
// Database operations ARE on the object
$user = User::find(1); // SELECT * FROM users WHERE id = 1
$user->name = 'Bob';
$user->save(); // UPDATE users SET name = 'Bob' WHERE id = 1
$user->delete(); // DELETE FROM users WHERE id = 1
$user->orders()->count(); // SELECT COUNT(*) FROM orders WHERE user_id = 1
// ============================================================
// DATAMAPPER — Doctrine style
// ============================================================
// Entity — pure PHP class, NO DB knowledge
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100)]
public string $name;
#[ORM\Column(length: 255, unique: true)]
public string $email;
#[ORM\OneToMany(targetEntity: Order::class, mappedBy: 'user')]
private \Doctrine\Common\Collections\Collection $orders;
// No save(), no find(), no delete() — this is a PLAIN class
// Business logic only
public function getDisplayName(): string { return "[{$this->id}] {$this->name}"; }
}
// EntityManager handles persistence (separate from domain)
$entityManager = // configured Doctrine EntityManager
$user = $entityManager->find(User::class, 1); // persistence via EntityManager
$user->name = 'Bob';
$entityManager->flush(); // UPDATE
$entityManager->remove($user);
$entityManager->flush(); // DELETE
// Repository — also separate from domain
$users = $entityManager->getRepository(User::class)->findBy(['active' => true]);
// KEY DIFFERENCE:
// ActiveRecord: $user->save() — object persists itself
// DataMapper: $em->flush() — external manager persists objects
//
// Unit testing User domain logic:
// ActiveRecord: Need DB or complex mocking
// DataMapper: new User('Alice', 'alice@x.com') — no DB needed at all!