Doctrine ORM vs Eloquent:PHPのORM設計思想を比較する
概要
PHPで代表的なORMといえば、Symfony圏で標準的に使われる Doctrine ORM と、Laravelに標準搭載される Eloquent の2つです。どちらもデータベースアクセスを抽象化する道具ですが、その設計思想は正反対と言っていいほど異なります。
この記事では、両者が採用するパターン(Data Mapper vs Active Record)を起点に、モデル定義・永続化・リレーション・クエリ・テスト容易性・パフォーマンスといった観点で比較し、「どちらを選ぶべきか」を判断するための材料を整理します。結論を先に言えば、優劣ではなく思想の違いです。
前提となる2つのパターン
Active Record(Eloquent)
- ドメインオブジェクト自身がDBアクセスの責務を持つ
- モデルクラスが「1行のレコード」と「その保存・取得ロジック」の両方を表す
$user->save()のように、オブジェクト自身が自分を保存する
Data Mapper(Doctrine)
- ドメインオブジェクトはDBを一切知らない(永続化非依存)
- オブジェクトとDBの間を「Mapper(EntityManager)」が仲介する
- エンティティは純粋なPHPオブジェクトで、保存は
$em->persist($user)のように外部が行う
この一点の違いが、以降のすべての差異に波及します。
モデル/エンティティ定義の比較
Eloquent:Modelを継承する
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
protected $fillable = ['name', 'email'];
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
Model を継承した時点で、save() / find() / where() などDBアクセス機能がすべて備わります。テーブル名やカラムは規約から自動推論され、明示的なマッピング定義は基本的に不要です。書き始めが速いのが特徴です。
Doctrine:Entityは属性でマッピングする
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 255)]
private string $name;
/** @var Collection<int, Post> */
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Post::class)]
private Collection $posts;
public function __construct()
{
$this->posts = new ArrayCollection();
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}
User は特定の基底クラスを継承せず、フレームワークへの依存もありません。エンティティ単体では自分を保存する手段を持たない代わりに、カプセル化やドメインロジックが自然に書けます。
永続化の比較
// Eloquent:オブジェクトが自分を保存する
$user = new User();
$user->name = 'Taro';
$user->save(); // 即SQL発行
// Doctrine:Unit of Work が変更を追跡する
$user = new User();
$user->setName('Taro');
$entityManager->persist($user);
$entityManager->flush(); // ここで初めてSQLが発行される
Doctrineの核心は Unit of Work です。persist() した瞬間にはDBへ行かず、flush() のタイミングで管理下のエンティティすべての変更差分をまとめて検出し、最適な順序でSQLを発行します。トランザクション境界が明確になる一方、「いつ・何がDBに書かれるのか」が直感的でなくなる学習コストがあります。
リレーションとN+1
// Eloquent:Eager Loading
$users = User::with('posts')->get();
// Doctrine:DQLのfetch join
$users = $entityManager->createQuery(
'SELECT u, p FROM App\Entity\User u LEFT JOIN u.posts p'
)->getResult();
Doctrineは独自クエリ言語 DQL(Doctrine Query Language) を持ち、テーブルではなくエンティティとプロパティに対してクエリを書きます。fetch join で関連を一括取得し、N+1を回避します。
テスト容易性
- Doctrine: エンティティがフレームワーク非依存の純PHPオブジェクトなので、DB不要でドメインロジックを単体テストできる。リポジトリをインターフェースでモックしやすく、DDD/クリーンアーキテクチャと相性が良い。
- Eloquent: モデルがDBと密結合するため統合テスト寄りになるが、Laravelは
RefreshDatabaseなどテスト基盤が非常に充実している。
パフォーマンス
- Eloquent: hydrateのオーバーヘッドが軽く、大量の単純な読み取りで有利なことが多い。
- Doctrine: Unit of Work の状態管理コストがある。大量読み取りでは
toIterable()・DTO直接取得・READ_ONLYヒント、バッチ処理ではclear()によるメモリ解放で最適化するのが定石。
選定の指針
| 状況 | 向いている方 |
|---|---|
| ドメインが複雑・ビジネスルールが厚い | Doctrine |
| DDD/クリーンアーキテクチャで永続化を分離したい | Doctrine |
| Symfonyを使っている | Doctrine |
| Laravel / 開発速度最優先 / CRUD中心 | Eloquent |
| チームがORMの学習に時間をかけられない | Eloquent |
注意点
- Laravelでも技術的にはDoctrineを組み込めるが、フレームワークの規約から外れるコストと恩恵を天秤にかける必要がある。
- 「Data Mapperだから正しい」ということはない。小規模CRUDに持ち込むとボイラープレートが増えるだけになりがち。
- 差が本当に出るのはドメインの複雑さが上がったとき。
まとめ
- Eloquentは Active Record、Doctrineは Data Mapper。この一点がすべての差の源。
- Eloquentは「速く書ける・規約に乗る・DBと密結合」。Doctrineは「永続化を分離できる・テストしやすい・学習コストが高い」。
- 選定基準は優劣ではなく、ドメインの複雑さと永続化をどこまで隠したいか。
- 迷ったら、使っているフレームワークの標準(Laravel→Eloquent / Symfony→Doctrine)に素直に従うのが最もコストが低い選択。
