tom__bo’s Blog

MySQL!! MySQL!! @tom__bo

Doctrine ORM チュートリアル(3)

前回に引き続きDoctrineのチュートリアルを進めていきます。

(参考: Getting Started with Doctrine — Doctrine 2 ORM 2 documentation)

 

Queries for Application Use-Cases

List of Bugs

 

前の例を使うことで、多少DBを利用する感覚ができたでしょう、しかし、要求された見た目の表現のために基本部分であるマッパーにどんなクエリーを発行する必要があるのか理解する必要があります。アプリケーションを開いたとき、バグはリスト上にページネートされて表示される必要があり、これが最初の読み込みだけのユースケースになります。

<?php
// list_bugs.php
require_once "bootstrap.php";

$dql = "SELECT b, e, r FROM Bug b JOIN b.engineer e JOIN b.reporter r ORDER BY b.created DESC";

$query = $entityManager->createQuery($dql);
$query->setMaxResults(30);
$bugs = $query->getResult();

foreach ($bugs as $bug) {
    echo $bug->getDescription()." - ".$bug->getCreated()->format('d.m.Y')."\n";
    echo "    Reported by: ".$bug->getReporter()->getName()."\n";
    echo "    Assigned to: ".$bug->getEngineer()->getName()."\n";
    foreach ($bug->getProducts() as $product) {
        echo "    Platform: ".$product->getName()."\n";
    }
    echo "\n";
}

 

このDQLクエリーの例では、1つの単純なSQL文で対象のエンジニアとレポーターとともに最新30のバグが呼び出されている。実行したときのアウトプットは以下です。

Something does not work! - 02.04.2010
    Reported by: beberlei
    Assigned to: beberlei
    Platform: My Product

 

 

Array Hydration of the Bug List

 

これまでのユースケースではそれぞれのオブジェクトインスタンスとして結果を取り出してきました。しかしDoctrineからはオブジェクトでしか取り出せないわけではありません。先の例のようなシンプルなリスト画面を作りたい場合、エンティティに対して読み出しのアクセスができればよく、そういった場合はオブジェクトからシンプルなPHPの配列にハイドレーションすることが出来ます。

 

ハイドレーションは素晴らしいプロセスで、ほしいものを取り出すだけの読み出しだけのリクエストではパフォーマンスにおいて利益をもたらすことがあるでしょう。

 

このarray hydrationを使うと、同じリスト画面を実装するのに以下のように書き直せます。

<?php
// list_bugs_array.php
require_once "bootstrap.php";

$dql = "SELECT b, e, r, p FROM Bug b JOIN b.engineer e ".
       "JOIN b.reporter r JOIN b.products p ORDER BY b.created DESC";
$query = $entityManager->createQuery($dql);
$bugs = $query->getArrayResult();

foreach ($bugs as $bug) {
    echo $bug['description'] . " - " . $bug['created']->format('d.m.Y')."\n";
    echo "    Reported by: ".$bug['reporter']['name']."\n";
    echo "    Assigned to: ".$bug['engineer']['name']."\n";
    foreach ($bug['products'] as $product) {
        echo "    Platform: ".$product['name']."\n";
    }
    echo "\n";
}

 

しかしDQLクエリには重大な違いがあり、それはbugに紐付いているプロダクトに対して追加のfetch-joinを加える必要があります。結果として、単純なselect文はかなり大きくなりましたが、ハイドレーションオブジェクトと比べてさらに効率的になりました。

 

 

Find by Primary Key

 

次のユースケースではプライマリキーによってBugを表示しています。これは前回のDQLにwhere句を用いれば可能ですが、EntityManagerにはプライマリキーを読み込んでくれる便利なメソッドがあります、これはすでにwriteのシナリオで見たものです。

<?php
// show_bug.php
require_once "bootstrap.php";

$theBugId = $argv[1];

$bug = $entityManager->find("Bug", (int)$theBugId);

echo "Bug: ".$bug->getDescription()."\n";
echo "Engineer: ".$bug->getEngineer()->getName()."\n";

 

エンジニアの名前がフェッチされています!何が起きたのでしょう?

 

プライマリキーによってbugを取り出しただけなので、DBからすぐにengineerとreporterが読み込まれることはありませんが、これはLazyLoadingプロキシにとって代わられています。これらのプロキシは最初のメソッドが呼び出されたときに裏で読み込まれています。

 

このプロキシが生成したコードは特定のプロキシディレクトリに見つけることができ、以下のようになっています。

<?php
namespace MyProject\Proxies;

/**
 * THIS CLASS WAS GENERATED BY THE DOCTRINE ORM. DO NOT EDIT THIS FILE.
 **/
class UserProxy extends \User implements \Doctrine\ORM\Proxy\Proxy
{
    // .. lazy load code here

    public function addReportedBug($bug)
    {
        $this->_load();
        return parent::addReportedBug($bug);
    }

    public function assignedToBug($bug)
    {
        $this->_load();
        return parent::assignedToBug($bug);
    }
}

 

それぞれのメソッドがプロキシを呼ぶ方法はDBからの遅延読み込みなのか見てみよう。

これを呼び出すと以下のようになります。

$ php show_bug.php 1
Bug: Something does not work!
Engineer: beberlei

 

Dashboad of User 

次のステップとして、ユーザが報告したもの、プログラマがアサインされたすべてのバグのリストを表示するダッシュボードビューを取り出してみます。ここでまたDQLを扱い、バウンドパラメータとWHERE句を利用していきます。

<?php
// dashboard.php
require_once "bootstrap.php";

$theUserId = $argv[1];

$dql = "SELECT b, e, r FROM Bug b JOIN b.engineer e JOIN b.reporter r ".
       "WHERE b.status = 'OPEN' AND (e.id = ?1 OR r.id = ?1) ORDER BY b.created DESC";

$myBugs = $entityManager->createQuery($dql)
                        ->setParameter(1, $theUserId)
                        ->setMaxResults(15)
                        ->getResult();

echo "You have created or assigned to " . count($myBugs) . " open bugs:\n\n";

foreach ($myBugs as $bug) {
    echo $bug->getId() . " - " . $bug->getDescription()."\n";

 

 

Number of Bugs

 

ここまででは、エンティティかそれらの配列の表現で取り出すことしかしていません。DoctrineではDQLを利用することで、エンティティでない取り出し方もサポートしています。これらの値はスカラーリザルト値と呼ばれていて、COUNT, SUM, MIN, MAX, AVG関数を使うことで集計された値等があります。

プロダクトからバグの集合の数を引いてくるためにこの知識が必要になります。

<?php
// products.php
require_once "bootstrap.php";

$dql = "SELECT p.id, p.name, count(b.id) AS openBugs FROM Bug b ".
       "JOIN b.products p WHERE b.status = 'OPEN' GROUP BY p.id";
$productBugs = $entityManager->createQuery($dql)->getScalarResult();

foreach ($productBugs as $productBug) {
    echo $productBug['name']." has " . $productBug['openBugs'] . " open bugs!\n";
}

 

 

Updating Entities

 

これらは単純なユースケースが要求から抜けています。エンジニアはバグをクローズできる必要があり、これは以下のようになります。

<?php
// src/Bug.php

class Bug
{
    public function close()
    {
        $this->status = "CLOSE";
    }
}
<?php
// close_bug.php
require_once "bootstrap.php";

$theBugId = $argv[1];

$bug = $entityManager->find("Bug", (int)$theBugId);
$bug->close();

$entityManager->flush();

 

DBからバグを引いてくるとき、DoctrineのUnitOfWorkの中で、IdentityMapに挿入されます。これはどんなにEntitiyManager#find()を呼んでもバグとこのidは一つしか存在しないことを意味しています。これはDQLを使ってハイドレートされたエンティティや、すでにIdentity Mapに存在するエンティティであっても検出します。

Flushが呼び出されたとき、identity mapのエンティティ中のEntityManagerループと値間の比較の実行はDBとエンティティが現在持っているこれらの値から取り出されます。

もし1つでもプロパティが異なれば、エンティティはアップデートされるように予定されます。カラムがアップデートされた時だけ、すべてのプロパティをアップデートするのと比較しすれば非常に効率の良い実行がなされます。

 

 

Entity Repositories

 

まだモデルからDoctrineのクエリロジックをどう分類するかに言及していませんでした。Doctrine1ではこの分離のためにDoctrine_Tableインスタンスという概念がありました。これと似たような概念はDoctrine2ではEntity Repositoriesと呼ばれDoctrineの中核でリポジトリパターンを統合しています。

 

どのエンティティも基本的にデフォルトのリポジトリを使っていて、それらのエンティティのインスタンスに対してクエリを使えるようにする便利なメソッド群を提供しています。これまでのProductエンティティを例にしてみましょう。もし、名前でクエリを発行したい時は以下のように使えます。

<?php
$product = $entityManager->getRepository('Product')
                         ->findOneBy(array('name' => $productName));

 

findOneBy()というメソッドはフィールドの配列化、外部キーとそれにマッチする値を必要とします。

 

または、もし条件に一致するエンティティ全てを引き出したいときはfindBy()を利用することが出来、例えば全てのclosedしたバグを得るには以下のようになります。

<?php
$bugs = $entityManager->getRepository('Bug')
                      ->findBy(array('status' => 'CLOSED'));

foreach ($bugs as $bug) {
    // do stuff
}

 

DQLと比較して、これらのメソッドは機能的にとても高速になります。DoctrineはデフォルトのEntityRepositoryを機能的に拡張したり、それに特殊なDQLのクエリロジックを付加する便利な方法を提供しています。このためにはDoctrine\ORM\EntityRepositoryサブクラスを作成する必要があり、これまでのケースだとBugRepositoryとこれまでに出てきたクエリの集合の中で必要になります。

<?php
// src/BugRepository.php

use Doctrine\ORM\EntityRepository;

class BugRepository extends EntityRepository
{
    public function getRecentBugs($number = 30)
    {
        $dql = "SELECT b, e, r FROM Bug b JOIN b.engineer e JOIN b.reporter r ORDER BY b.created DESC";

        $query = $this->getEntityManager()->createQuery($dql);
        $query->setMaxResults($number);
        return $query->getResult();
    }

    public function getRecentBugsArray($number = 30)
    {
        $dql = "SELECT b, e, r, p FROM Bug b JOIN b.engineer e ".
               "JOIN b.reporter r JOIN b.products p ORDER BY b.created DESC";
        $query = $this->getEntityManager()->createQuery($dql);
        $query->setMaxResults($number);
        return $query->getArrayResult();
    }

    public function getUsersBugs($userId, $number = 15)
    {
        $dql = "SELECT b, e, r FROM Bug b JOIN b.engineer e JOIN b.reporter r ".
               "WHERE b.status = 'OPEN' AND e.id = ?1 OR r.id = ?1 ORDER BY b.created DESC";

        return $this->getEntityManager()->createQuery($dql)
                             ->setParameter(1, $userId)
                             ->setMaxResults($number)
                             ->getResult();
    }

    public function getOpenBugsByProduct()
    {
        $dql = "SELECT p.id, p.name, count(b.id) AS openBugs FROM Bug b ".
               "JOIN b.products p WHERE b.status = 'OPEN' GROUP BY p.id";
        return $this->getEntityManager()->createQuery($dql)->getScalarResult();
    }
}

 

このクエリロジックを$this->getEntityManager()->getRepository(‘Bug’)を通して使えるようにするために、多少のメタデータを適用する必要があります。

<?php
/**
 * @Entity(repositoryClass="BugRepository")
 * @Table(name="bugs")
 **/
class Bug
{
    //...
}

 

これで全てのクエリロジックを削除して、EntityRepositoryを使えばいいことになりました。例として、最初のユースケースであった”List of Bugs”は以下のようになります。

<?php
// list_bugs_repository.php
require_once "bootstrap.php";

$bugs = $entityManager->getRepository('Bug')->getRecentBugs();

foreach ($bugs as $bug) {
    echo $bug->getDescription()." - ".$bug->getCreated()->format('d.m.Y')."\n";
    echo "    Reported by: ".$bug->getReporter()->getName()."\n";
    echo "    Assigned to: ".$bug->getEngineer()->getName()."\n";
    foreach ($bug->getProducts() as $product) {
        echo "    Platform: ".$product->getName()."\n";
    }
    echo "\n";
}

 

EntityRepositoriesを使うことによって、特定のクエリロジックとモデルをいっしょにすることを避ける事ができます。更にはアプリケーション中で、クエリロジックを簡単に再利用することができるようになったのがわかるでしょう。

 

 

Conclusion

 

このチュートリアルはこれで終了だ。楽しんでくれたでしょうか。このチュートリアルのコンテンツは徐々に追加されていく予定で、追加予定のトピックは以下です。

・ アソシエーションマッピング

・ UnitOfWork内のイベントトリガーライフサイクル

・ コレクションの並び替え

個々で議論した項目の詳細にしてはそれぞれのマニュアルチャプターで見つけることができるでしょう。

 

 

と。。。

 

ようやくひと通りチュートリアルを実行していくことが出来ました。

(4)ではこのチュートリアルのまとめとEntityRepositoryへの書換を行っていこうと思います。

 

Tsuduku ⊂゚U┬───┬~