Doctrine ORM

Doctrine ORMは「PHPオブジェクトの透過的な永続化」を提供するPHP向けの高機能ORMライブラリです。エンティティをデータベーステーブルにマッピングするメタデータシステム、One-to-One、One-to-Many、Many-to-Many等の包括的なアソシエーション管理、PHP8+属性・XML・PHP記述による柔軟なマッピング定義を特徴とします。メタデータキャッシュ、クエリキャッシュ、結果キャッシュによる高いパフォーマンス、プロキシオブジェクトによる遅延ローディング、DQLによる高度なクエリ処理により、エンタープライズレベルのPHPアプリケーション開発を強力にサポートします。

ORMPHPエンティティマッピングアソシエーションメタデータキャッシュDBAL

GitHub概要

doctrine/orm

Doctrine Object Relational Mapper (ORM)

スター10,074
ウォッチ247
フォーク2,543
作成日:2010年4月6日
言語:PHP
ライセンス:MIT License

トピックス

hacktoberfest

スター履歴

doctrine/orm Star History
データ取得日時: 2025/7/17 10:32

ライブラリ

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パターンと比較したデータマッパーパターンの複雑性

参考ページ

書き方の例

基本セットアップ

# 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());