Doctrine ORM
Doctrine ORMは「PHPオブジェクトの透過的な永続化」を提供するPHP向けの高機能ORMライブラリです。エンティティをデータベーステーブルにマッピングするメタデータシステム、One-to-One、One-to-Many、Many-to-Many等の包括的なアソシエーション管理、PHP8+属性・XML・PHP記述による柔軟なマッピング定義を特徴とします。メタデータキャッシュ、クエリキャッシュ、結果キャッシュによる高いパフォーマンス、プロキシオブジェクトによる遅延ローディング、DQLによる高度なクエリ処理により、エンタープライズレベルのPHPアプリケーション開発を強力にサポートします。
GitHub概要
doctrine/orm
Doctrine Object Relational Mapper (ORM)
トピックス
スター履歴
ライブラリ
Doctrine ORM
概要
Doctrine ORMは「PHPオブジェクトの透過的な永続化」を提供するPHP向けの高機能ORMライブラリです。エンティティをデータベーステーブルにマッピングするメタデータシステム、One-to-One、One-to-Many、Many-to-Many等の包括的なアソシエーション管理、PHP8+属性・XML・PHP記述による柔軟なマッピング定義を特徴とします。メタデータキャッシュ、クエリキャッシュ、結果キャッシュによる高いパフォーマンス、プロキシオブジェクトによる遅延ローディング、DQLによる高度なクエリ処理により、エンタープライズレベルのPHPアプリケーション開発を強力にサポートします。
詳細
Doctrine ORM 2025年版はPHP8.1+の最新機能を活用し、enumType、プロパティフック、型推論等の現代的なPHP機能を完全サポートしています。AttributeDriver、XMLDriver、PHPDriverによる多様なメタデータ定義方式、ClassMetadataFactoryによる効率的なマッピング情報管理、継承戦略(Single Table、Class Table、Mapped Superclass)の完全サポートを実装。PSR-6互換キャッシュアダプター、カスタムDQL関数、イベントリスナー、カスタムハイドレーションモード等の拡張ポイントにより、複雑なビジネス要件に対応する高度なカスタマイゼーションが可能です。
主な特徴
- 透過的オブジェクト永続化: PHPオブジェクトの自動データベースマッピング
- 包括的アソシエーション: One-to-One、One-to-Many、Many-to-Many完全サポート
- 柔軟なマッピング: PHP8+属性、XML、PHP記述による多様な定義方式
- 高度なキャッシング: メタデータ、クエリ、結果キャッシュの3層構造
- プロキシとレイジーローディング: 効率的なメモリ使用とパフォーマンス最適化
- DQLクエリ言語: オブジェクト指向の高度なクエリ処理
メリット・デメリット
メリット
- PHPエコシステムで最も成熟したORMとしての豊富な機能と安定性
- 属性・XML・PHPによる柔軟なマッピング定義とメタデータ管理
- 包括的なアソシエーション管理と複雑なリレーションシップ処理能力
- PSR-6準拠の3層キャッシュシステムによる高いパフォーマンス
- Symfony、Laravel等主要フレームワークとの優れた統合性
- 継承マッピング、カスタム型、イベントシステム等の拡張機能
デメリット
- 大規模なライブラリによる学習コストとメモリ使用量の増加
- 設定とメタデータ定義の複雑さによる初期セットアップの困難さ
- ORM特有のN+1問題や遅延ローディングによるパフォーマンス課題
- プロキシ生成とメタデータ処理による実行時オーバーヘッド
- 複雑なクエリでは生SQLやDBAL使用が必要な場合
- Active Recordパターンと比較したデータマッパーパターンの複雑性
参考ページ
- Doctrine ORM 公式ドキュメント - メインドキュメント
- Doctrine ORM GitHub - ソースコードとイシュー
- Doctrine DBAL ドキュメント - データベース抽象化層
- Symfony Doctrine ガイド - Symfony統合ガイド
書き方の例
基本セットアップ
# Composer経由でのインストール
composer require doctrine/orm
# Doctrine DBAL(データベース抽象化層)
composer require doctrine/dbal
# 開発時の便利ツール
composer require --dev doctrine/doctrine-fixtures-bundle
composer require --dev doctrine/doctrine-migrations-bundle
<?php
// bootstrap.php - Doctrine設定
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
require_once "vendor/autoload.php";
// Entity metadata paths
$paths = [__DIR__."/src/Entity"];
$isDevMode = true;
// database configuration
$dbParams = [
'driver' => 'pdo_mysql',
'user' => 'root',
'password' => '',
'dbname' => 'myapp',
'host' => 'localhost',
'charset' => 'utf8mb4',
];
// ORM configuration
$config = ORMSetup::createAttributeMetadataConfiguration($paths, $isDevMode);
$connection = DriverManager::getConnection($dbParams, $config);
$entityManager = new EntityManager($connection, $config);
モデル定義と基本操作
<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
#[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;
#[ORM\Column(type: 'string', length: 255, unique: true)]
private string $email;
#[ORM\Column(type: 'datetime')]
private \DateTime $createdAt;
#[ORM\OneToMany(mappedBy: 'user', targetEntity: Post::class)]
private Collection $posts;
public function __construct()
{
$this->posts = new ArrayCollection();
$this->createdAt = new \DateTime();
}
// Getters and Setters
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
public function getPosts(): Collection
{
return $this->posts;
}
public function addPost(Post $post): self
{
if (!$this->posts->contains($post)) {
$this->posts[] = $post;
$post->setUser($this);
}
return $this;
}
}
<?php
// src/Entity/Post.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'posts')]
class Post
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 255)]
private string $title;
#[ORM\Column(type: 'text')]
private string $content;
#[ORM\Column(type: 'boolean')]
private bool $published = false;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'posts')]
#[ORM\JoinColumn(nullable: false)]
private User $user;
#[ORM\Column(type: 'datetime')]
private \DateTime $createdAt;
public function __construct()
{
$this->createdAt = new \DateTime();
}
// Getters and Setters
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getContent(): string
{
return $this->content;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function isPublished(): bool
{
return $this->published;
}
public function setPublished(bool $published): self
{
$this->published = $published;
return $this;
}
public function getUser(): User
{
return $this->user;
}
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
}
高度なクエリ操作
<?php
// src/Repository/UserRepository.php
namespace App\Repository;
use App\Entity\User;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
class UserRepository extends EntityRepository
{
public function findByEmailDomain(string $domain): array
{
return $this->createQueryBuilder('u')
->where('u.email LIKE :domain')
->setParameter('domain', '%@' . $domain)
->orderBy('u.createdAt', 'DESC')
->getQuery()
->getResult();
}
public function findUsersWithPosts(): array
{
return $this->createQueryBuilder('u')
->innerJoin('u.posts', 'p')
->addSelect('p')
->where('p.published = :published')
->setParameter('published', true)
->getQuery()
->getResult();
}
public function getUsersWithPostCount(): array
{
return $this->createQueryBuilder('u')
->select('u', 'COUNT(p.id) as postCount')
->leftJoin('u.posts', 'p')
->groupBy('u.id')
->having('COUNT(p.id) > 0')
->orderBy('postCount', 'DESC')
->getQuery()
->getResult();
}
public function findByCustomCriteria(array $criteria): QueryBuilder
{
$qb = $this->createQueryBuilder('u');
if (!empty($criteria['name'])) {
$qb->andWhere('u.name LIKE :name')
->setParameter('name', '%' . $criteria['name'] . '%');
}
if (!empty($criteria['email'])) {
$qb->andWhere('u.email = :email')
->setParameter('email', $criteria['email']);
}
if (!empty($criteria['dateFrom'])) {
$qb->andWhere('u.createdAt >= :dateFrom')
->setParameter('dateFrom', $criteria['dateFrom']);
}
return $qb;
}
}
リレーション操作
<?php
// 基本的なCRUD操作とリレーション管理
require_once "bootstrap.php";
use App\Entity\User;
use App\Entity\Post;
// EntityManagerの取得
$entityManager = getEntityManager();
// CREATE - ユーザーと投稿の作成
$user = new User();
$user->setName('田中太郎');
$user->setEmail('[email protected]');
$post1 = new Post();
$post1->setTitle('初投稿');
$post1->setContent('Doctrine ORMの使い方について学習中です。');
$post1->setPublished(true);
$post1->setUser($user);
$post2 = new Post();
$post2->setTitle('続・Doctrine入門');
$post2->setContent('エンティティリレーションの実装方法。');
$post2->setPublished(false);
$post2->setUser($user);
// 永続化
$entityManager->persist($user);
$entityManager->persist($post1);
$entityManager->persist($post2);
$entityManager->flush();
echo "User created with ID: " . $user->getId() . "\n";
// READ - データの取得
$userRepository = $entityManager->getRepository(User::class);
$postRepository = $entityManager->getRepository(Post::class);
// IDによる取得
$foundUser = $userRepository->find($user->getId());
echo "Found user: " . $foundUser->getName() . "\n";
// 条件による検索
$users = $userRepository->findBy(['email' => '[email protected]']);
foreach ($users as $u) {
echo "User: " . $u->getName() . " (" . $u->getEmail() . ")\n";
}
// 関連データの取得(遅延ローディング)
$userPosts = $foundUser->getPosts();
echo "User has " . $userPosts->count() . " posts\n";
foreach ($userPosts as $post) {
echo " - " . $post->getTitle() . " (Published: " . ($post->isPublished() ? 'Yes' : 'No') . ")\n";
}
// UPDATE - データの更新
$foundUser->setName('田中次郎');
$post1->setPublished(false);
$entityManager->flush(); // 変更をコミット
// DELETE - データの削除
$postToDelete = $postRepository->find($post2->getId());
if ($postToDelete) {
$entityManager->remove($postToDelete);
$entityManager->flush();
echo "Post deleted\n";
}
実用例
<?php
// 高度なクエリとトランザクション処理
// DQLクエリの使用
$dql = "SELECT u, p FROM App\Entity\User u
LEFT JOIN u.posts p
WHERE u.email LIKE :domain
AND p.published = :published
ORDER BY u.createdAt DESC";
$query = $entityManager->createQuery($dql);
$query->setParameter('domain', '%@example.com');
$query->setParameter('published', true);
$query->setMaxResults(10);
$results = $query->getResult();
foreach ($results as $user) {
echo "User: " . $user->getName() . "\n";
foreach ($user->getPosts() as $post) {
if ($post->isPublished()) {
echo " Published: " . $post->getTitle() . "\n";
}
}
}
// Native SQLクエリ
$sql = "SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE p.published = 1
GROUP BY u.id
ORDER BY post_count DESC";
$stmt = $entityManager->getConnection()->prepare($sql);
$result = $stmt->executeQuery();
$data = $result->fetchAllAssociative();
foreach ($data as $row) {
echo $row['name'] . ": " . $row['post_count'] . " posts\n";
}
// トランザクション処理
$entityManager->beginTransaction();
try {
// 複数の操作をトランザクション内で実行
$newUser = new User();
$newUser->setName('鈴木花子');
$newUser->setEmail('[email protected]');
$newPost = new Post();
$newPost->setTitle('トランザクションテスト');
$newPost->setContent('複数の操作を安全に実行。');
$newPost->setUser($newUser);
$newPost->setPublished(true);
$entityManager->persist($newUser);
$entityManager->persist($newPost);
// すべての操作が成功した場合のみコミット
$entityManager->flush();
$entityManager->commit();
echo "Transaction completed successfully\n";
} catch (\Exception $e) {
// エラーが発生した場合はロールバック
$entityManager->rollback();
echo "Transaction failed: " . $e->getMessage() . "\n";
}
// バッチ処理(大量データの効率的な処理)
$batchSize = 100;
$counter = 0;
$query = $entityManager->createQuery('SELECT u FROM App\Entity\User u');
$iterableResult = $query->toIterable();
foreach ($iterableResult as $user) {
// 各ユーザーに対する処理
$user->setName($user->getName() . ' [Updated]');
if (++$counter % $batchSize === 0) {
// バッチサイズに達したらフラッシュしてクリア
$entityManager->flush();
$entityManager->clear();
}
}
// 残りの変更をフラッシュ
$entityManager->flush();
$entityManager->clear();
// カスタムイベントリスナー例
use Doctrine\ORM\Events;
use Doctrine\Common\EventSubscriber;
use Doctrine\Persistence\Event\LifecycleEventArgs;
class PostSubscriber implements EventSubscriber
{
public function getSubscribedEvents(): array
{
return [
Events::prePersist,
Events::postPersist,
];
}
public function prePersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if ($entity instanceof Post) {
// 投稿作成時に自動的にスラッグを生成
$slug = strtolower(str_replace(' ', '-', $entity->getTitle()));
// $entity->setSlug($slug);
echo "Creating post: " . $entity->getTitle() . "\n";
}
}
public function postPersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if ($entity instanceof Post) {
echo "Post created with ID: " . $entity->getId() . "\n";
}
}
}
// イベントリスナーの登録
$eventManager = $entityManager->getEventManager();
$eventManager->addEventSubscriber(new PostSubscriber());