シムノート

PHPフレームワークSymfonyの学習帳

ユーザ用ツール

サイト用ツール


サイドバー

メニュー



このエントリーをはてなブックマークに追加

blog:2015-12-30:doctrine入門_queries_for_application_use-cases

Doctrine入門 : (5) ユースケースのクエリー

List of Bugs

CRUDジェネレータで作成したBugの一覧表示を修正していきます。
DQLを使って、複雑なクエリーの作成を可能にします。
また、KnpPaginatorBundleを使って、ページ制御と一覧表のソート機能を実装します。

KnpPaginatorBundle

インストール

$ composer require knplabs/knp-paginator-bundle

public function registerBundles()
{
    return array(
        // ...
        new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),
        // ...
    );
}

設定

# ...

knp_paginator:
    template:
        # Bootstrap3用テンプレート
        pagination: KnpPaginatorBundle:Pagination:twitter_bootstrap_v3_pagination.html.twig

Controllerの修正

indexAction()をDQLとKnpPaginatorBundleを使用するように修正します。

// ...

use Doctrine\ORM\Query;
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination;

// ...
class BugController extends Controller
{
    // ...
    public function indexAction(Request $request)
    {
        $dql = "SELECT b, e, r FROM AppBundle:Bug b " .
               "JOIN b.engineer e JOIN b.reporter r " .
               "ORDER BY b.created DESC";
        /** @var Query $query */
        $query = $this->getDoctrine()->getManager()->createQuery($dql);

        $paginator = $this->get('knp_paginator');        
        /** @var SlidingPagination $pagination */
        $pagination = $paginator->paginate(
            $query,
            $request->query->getInt('page', 1), // page number
            5  // limit per page
        );
        // 上記 paginate()は内部で以下の2行と同様の処理を行い結果を返します。
        // $query->setMaxResults(5);
        // $bugs = $query->getResult();

        // デバッグコード(ハイドレーションの確認)
        dump($query->getHydrationMode());
        dump($pagination->getItems());
        
        return $this->render('bug/index.html.twig', array(
            'pagination' => $pagination,
        ));
    }
    // ...
}

DQLを使用することで複雑なクエリーをSQLと同様の方法で作成することができます。SQLとの違いは、テーブルとカラムの代わりに、エンティティクラスとそのプロパティを使用することです。

DoctrineではDBの検索結果をエンティティオブジェクトや階層化された配列に変換することをHydration(ハイドレーション)と呼んでいます。このハイドレーションの状態を確認する為に、dump()を使って、デバッグコードを入れました。後ほど動作確認の時に確認してみます。

Viewの修正

bug/index.html.twigを以下のように修正します。

{% extends 'base.html.twig' %}

{% block body %}
    <h1>Bug list</h1>

    <table class="table table-striped table-hover table-bordered">
        <thead>
            <tr>
                {# ソート可能な一覧表を表示する #}
                <th {% if pagination.isSorted('b.id') %} class="sorted" {% endif %}>
                    {{ knp_pagination_sortable(pagination, 'Id', 'b.id') }}
                </th>
                <th {% if pagination.isSorted('b.description') %} class="sorted" {% endif %}>
                    {{ knp_pagination_sortable(pagination, 'Description', 'b.description') }}
                </th>
                <th {% if pagination.isSorted('b.status') %} class="sorted" {% endif %}>
                    {{ knp_pagination_sortable(pagination, 'Status', 'b.status') }}
                </th>
                <th {% if pagination.isSorted('b.created') %} class="sorted" {% endif %}>
                    {{ knp_pagination_sortable(pagination, 'Created', 'b.created') }}
                </th>
                <th>Reporter</th>
                <th>Engineer</th>
                <th>Products</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
        {% for bug in pagination %}
            <tr>
                <td><a href="{{ path('bug_show', { 'id': bug.id }) }}">{{ bug.id }}</a></td>
                <td>{{ bug.description }}</td>
                <td>{{ bug.status }}</td>
                <td>{% if bug.created %}{{ bug.created|date('Y-m-d H:i:s') }}{% endif %}</td>
                <td>{{ bug.reporter.name }}</td>
                <td>{{ bug.engineer.name }}</td>
                <td>
                    {# プロダクト名を連結して表示する #}
                    {{ bug.products|join(', ') }}
                </td>
                <td>
                    <a href="{{ path('bug_show', { 'id': bug.id }) }}" class="btn btn-default btn-xs" role="button">show</a>
                    <a href="{{ path('bug_edit', { 'id': bug.id }) }}" class="btn btn-primary btn-xs" role="button">edit</a>
                </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
    <div>
        {# ページ制御のボタン群を表示する #}
        {{ knp_pagination_render(pagination) }}
    </div>

    <a href="{{ path('bug_new') }}" class="btn btn-primary" role="button">Create a new entry</a>
{% endblock %}

39行目のbug.products|join(', ')を可能にする為に、Productエンティティに__toString()を追加します。

// ...
class Product
{
    // ...
    public function __toString()
    {
        return $this->getName();
    }
}

動作確認

http://localhost:8000/bug にアクセスして、Bug一覧の動作確認を行います。

また、ControllerのindexAction()に仕込んだdump()の出力結果も確認してみます。
Bug一覧画面を表示した状態で画面下部にあるデバッグバーをクリックしてプロファイラーを表示します。左のメニューでDebugを選択すると、以下のようにdump()の出力結果が表示されます。

debug_hydrate_object.jpg

$query->getHydrationMode()ということはDBの検索結果をエンティティオブジェクトに変換する事を表しています。

const HYDRATE_OBJECT = 1;
const HYDRATE_ARRAY = 2;

$pagination->getItems()を見てみると、クエリーの検索結果としてBugエンティティオブジェクトの配列が返ってきていることが分かります。

Array Hydration of the Bug List

今まではクエリーの結果は全てオブジェクト(Hydrate Object)で取得してきました。しかし、上記の一覧表示のような読み込みだけ出来れば良いケースではHydraete Objectはオーバースペックでその分パフォーマンスも落ちます。クエリー結果を配列で取得する(Hydrate Array)方法で、Bug一覧表示を書き換えてみます。

Controllerの修正

indexAction()を以下の様に修正します。

// ...

use Doctrine\ORM\Query;
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination;

// ...
class BugController extends Controller
{
    // ...
    public function indexAction(Request $request)
    {
        $dql = "SELECT b, e, r, p FROM AppBundle:Bug b " .
               "JOIN b.engineer e JOIN b.reporter r JOIN b.products p " .
               "ORDER BY b.created DESC";
        /** @var Query $query */
        $query = $this->getDoctrine()->getManager()->createQuery($dql);
        $query->setHydrationMode(Query::HYDRATE_ARRAY);

        $paginator = $this->get('knp_paginator');        
        /** @var SlidingPagination $pagination */
        $pagination = $paginator->paginate(
            $query,
            $request->query->getInt('page', 1), // page number
            5  // limit per page
        );
        // 上記 paginate()は内部で以下の2行と同様の処理を行い結果を返します。
        // $query->setMaxResults(5);
        // $bugs = $query->getArrayResult();

        // デバッグコード(ハイドレーションの確認)
        dump($query->getHydrationMode());
        dump($pagination->getItems());
        
        return $this->render('bug/index.html.twig', array(
            'pagination' => $pagination,
        ));
    }
    // ...
}

setHydrationMode()Query::HYDRATE_ARRAYをセットすることで、DBの検索結果を配列にすることができます。

DQLにProductのJOINを追加しました。この1行のクエリーは少し大きいですが、Hydrate Objectよりはまだ効率的に動きます。

Viewの修正

bug/index.html.twigを以下のように修正します。

{% extends 'base.html.twig' %}

{% block body %}
    <h1>Bug list</h1>

    <table class="table table-striped table-hover table-bordered">
        <thead>
            {# ... #}
        </thead>
        <tbody>
        {% for bug in pagination %}
            <tr>
                {# ... #}
                <td>
                    {# プロダクト名を連結して表示する #}
                    {% set str_products = [] %}
                    {% for product in bug.products %}
                        {% set str_products = str_products|merge([product.name]) %}
                    {% endfor %}
                    {{ str_products|join(', ') }}
                </td>
                {# ... #}
            </tr>
        {% endfor %}
        </tbody>
    </table>
    {# ... #}
{% endblock %}

動作確認

http://localhost:8000/bug にアクセスして動作確認を行います。

ControllerのindexAction()に仕込んだdump()の出力結果も確認してみます。

debug_hydrate_array.jpg

$query->getHydrationMode()2(Hydrate Array)になっています。

$pagination->getItems()を見てみると、クエリーの検索結果としてBugの情報が配列にセットされて返って来ていることが分かります。

Find by Primary Key

Bug表示画面でreporterengineer,productsを表示するように修正します。

Controllerの修正

コントローラーは特に修正する所は在りませんが、後ほど動作確認の時に見るdump()だけ仕込んでおきます。

また、CURDジェネレータで作成したshowAction()では@ParamConverterを使って、コードが省略されていることを忘れないでください。

// ...
class BugController extends Controller
    // ...
    
    /**
     * Lists all Bug entities.
     *
     * @Route("/", name="bug_index")
     * @Method("GET")
     * 
     * // ここに以下の行が省略されています。
     * // @ParamConverter("bug", class="AppBundl:Bug")
     */
    public function showAction(Bug $bug)
    {
        // @ParamConverterの機能を使って、以下と同様の処理が行われています。
        // 
        // $repository = $this->getDoctrine()->getRepository('AppBundle:Bug');
        // $bug = $repository->find($id);        
    
        $deleteForm = $this->createDeleteForm($bug);

        dump(get_class($bug->getEngineer()));

        return $this->render('bug/show.html.twig', array(
            'bug' => $bug,
            'delete_form' => $deleteForm->createView(),
        ));
    }
    // ...
}

ここでもDQLを使うことはできますが、idでオブジェクトを取得できるfind()の方が便利です。取得するデータも1件なので、パフォーマンスを気にする必要も在りません。

Viewの修正

reporterengineer,productsの表示を追加します。

{% extends 'base.html.twig' %}

{% block body %}
    <h1>Bug</h1>

    <table class="table table-striped table-hover table-bordered">
        <tbody>
            {# ... #}
            <tr>
                <th>Reporter</th>
                <td>{{ bug.reporter.name }}</td>
            </tr>
            <tr>
                <th>Engineer</th>
                <td>{{ bug.engineer.name }}</td>
            </tr>
            <tr>
                <th>Products</th>
                <td>{{ bug.products|join(', ') }}</td>
            </tr>
        </tbody>
    </table>

    {# ... #}
{% endblock %}

動作確認

http://localhost:8000/bug にアクセスして任意のBugのShowボタンをクリックします。 Bugのreporterengineer,productsが表示されるか確認します。

LayzyLoading(遅延ローディング)

先ほどshowAction()に仕込んでおいたdump(get_class($bug->getEngineer()))の出力をプロファイラで確認して見ます。

debug_layzy_proxy.jpg

AppBundle\Entity\Userが表示されるかと思い気や、Proxies\...といった物が表示されました。これはいったい何でしょう?

実は、Bugエンティティを読み込んだ直後には、Bugエンティティが所有しているreporterengineerproductsの各要素であるproductは、まだ読み込まれていません。その代わりに遅延ローディングプロキシ(LazyLoading proxies)がセットされています。getReporter()getEngineer()が実行された時に初めてDBからデータを読み込む仕組みになっています。

もう一つ、プロファイラを使って遅延ロードの動きを確認してみます。
先ほどのViewの修正前と修正後のSQLの実行状況をプロファイラで見比べます。

View変更前(reporter, engineer, productsの表示していない時) debug_layzy_proxy1.jpg

View変更後(reporter, engineer, productsの表示した時) debug_layzy_proxy2.jpg

前者はbugテーブルを検索するSQLが1回だけ発行されています。 後者はuserテーブルの検索が2回(reporterとengineer)とproductテーブルの検索が1回加わり、合計で4回のSQLが発行されています。 この事から、reporterやengineer、productは必要となった時に、初めてロードされたことが分かります。

遅延ローディングは実際には利用されないエンティティのリファレンスに対して、余計なSQLを発行しないことで負荷を減らしています。一方で、必ずエンティティのリファレンスを使うことが分かっているような場合で、一覧表表示のように読み込む件数が多い時には大量にSQLを発行してしまい、著しくパフォーマンスを下げてしまいます。そのような場合はDQLでJOINを使って1回のSQLで全てのデータをロードするように実装する必要があります。

LayzyLodingの概念は以下のサイトが参考になります。

http://capsctrl.que.jp/kdmsnr/wiki/PofEAA/?LazyLoad


Comments



94 +6 = ?
blog/2015-12-30/doctrine入門_queries_for_application_use-cases.txt · 最終更新: 2016/01/20 06:57 by tsubo