0

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: User model → users table.
  • Row = instance: Each User object 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 User is just public string $name. No extends Model.
  • Domain objects are truly portable: Unit test User with 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!