Extending DQL in Doctrine ORM: Custom AST Walkers

The Doctrine Query Language (DQL) is a proprietary sql-dialect that substitutes tables and columns for Entity names and their fields. Using DQL you write a query against the database using your entities. With the help of the metadata you can write very concise, compact and powerful queries that are then translated into SQL by the Doctrine ORM.

Doctrine Query Language (DQL) は、エンティティ名とそのフィールドをテーブルと列に置き換える独自の SQL 方言です。DQL を使用すると、エンティティを使用してデータベースに対するクエリを記述できます。メタデータの助けを借りて、Doctrine ORM によって SQL に変換される非常に簡潔でコンパクトで強力なクエリを書くことができます。

In Doctrine 1 the DQL language was not implemented using a real parser. This made modifications of the DQL by the user impossible. Doctrine ORM in contrast has a real parser for the DQL language, which transforms the DQL statement into an Abstract Syntax Tree and generates the appropriate SQL statement for it. Since this process is deterministic Doctrine heavily caches the SQL that is generated from any given DQL query, which reduces the performance overhead of the parsing process to zero.

Doctrine 1 では、DQL 言語はリアルパーサーを使用して実装されていませんでした。これにより、ユーザーによる DQL の変更が不可能になりました。対照的に、Doctrine ORM には DQL 言語用の実際のパーサーがあり、DQL ステートメントを抽象構文ツリーに変換し、適切な SQL ステートメントを生成します。このプロセスは決定論的であるため、Doctrine は特定の DQL クエリから生成された SQL を大量にキャッシュし、解析プロセスのパフォーマンスのオーバーヘッドをゼロに減らします。

You can modify the Abstract syntax tree by hooking into DQL parsing process by adding a Custom Tree Walker. A walker is an interface that walks each node of the Abstract syntax tree, thereby generating the SQL statement.

カスタム ツリー ウォーカーを追加して DQL 解析プロセスにフックすることで、抽象構文ツリーを変更できます。 walker は、Abstract 構文ツリーの各ノードをウォークし、SQL ステートメントを生成するインターフェイスです。

There are two types of custom tree walkers that you can hook into the DQL parser:

DQL パーサーにフックできるカスタム ツリー ウォーカーには、次の 2 種類があります。

  • An output walker. This one actually generates the SQL, and there is only ever one of them. We implemented the default SqlWalker implementation for it.

    出力ウォーカー。これは実際に SQL を生成しますが、そのうちの 1 つしかありません。そのためのデフォルトの SqlWalkerimplementation を実装しました。

  • A tree walker. There can be many tree walkers, they cannot generate the SQL, however they can modify the AST before its rendered to SQL.

    ツリーウォーカー。多くのツリー ウォーカーが存在する可能性があります。SQL を生成することはできませんが、SQL にレンダリングする前に AST を変更することはできます。

Now this is all awfully technical, so let me come to some use-cases fast to keep you motivated. Using walker implementation you can for example:

これはすべて非常に技術的なものなので、モチベーションを維持するためにいくつかのユースケースをすぐに紹介しましょう. walker 実装を使用すると、たとえば次のことができます。

  • Modify the AST to generate a Count Query to be used with a paginator for any given DQL query.

    AST を変更して、特定の DQL クエリの apaginator で使用されるカウント クエリを生成します。

  • Modify the Output Walker to generate vendor-specific SQL (instead of ANSI).

    Output Walker を変更して、(ANSI ではなく) ベンダー固有の SQL を生成します。

  • Modify the AST to add additional where clauses for specific entities (example ACL, country-specific content…)

    AST を変更して、特定のエンティティ (ACL、国固有のコンテンツなど) の where 句を追加します。

  • Modify the Output walker to pretty print the SQL for debugging purposes.

    出力ウォーカーを変更して、デバッグ目的で SQL をきれいに出力します。

In this cookbook-entry I will show examples of the first two points. There are probably much more use-cases.

このクックブック エントリでは、最初の 2 つのポイントの例を示します。おそらくもっと多くのユースケースがあります。

Generic count query for pagination

Say you have a blog and posts all with one category and one author. A query for the front-page or any archive page might look something like:

1 つのカテゴリと 1 つの作成者を持つブログと投稿があるとします。フロント ページまたはアーカイブ ページのクエリは次のようになります。

SELECT p, c, a FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...

Now in this query the blog post is the root entity, meaning it’s the one that is hydrated directly from the query and returned as an array of blog posts. In contrast the comment and author are loaded for deeper use in the object tree.

このクエリでは、ブログ投稿がルート エンティティです。つまり、クエリから直接ハイドレートされ、ブログ投稿の配列として返されます。対照的に、コメントと作成者は、オブジェクト ツリーでより深く使用するためにロードされます。

A pagination for this query would want to approximate the number of posts that match the WHERE clause of this query to be able to predict the number of pages to show to the user. A draft of the DQL query for pagination would look like:

このクエリのページネーションでは、このクエリの WHERE 句に一致する投稿数を概算して、ユーザーに表示するページ数を予測できるようにする必要があります。ページネーション用の DQLquery のドラフトは次のようになります。

SELECT count(DISTINCT p.id) FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...

Now you could go and write each of these queries by hand, or you can use a tree walker to modify the AST for you. Let’s see how the API would look for this use-case:

これらの各クエリを手動で記述したり、ツリー ウォーカーを使用して AST を変更したりできます。このユースケースで theAPI がどのように見えるか見てみましょう。

<?php
$pageNum = 1;
$query = $em->createQuery($dql);
$query->setFirstResult( ($pageNum-1) * 20)->setMaxResults(20);

$totalResults = Paginate::count($query);
$results = $query->getResult();

The Paginate::count(Query $query) looks like:

Paginate::count(Query $query) は次のようになります。

<?php
class Paginate
{
    static public function count(Query $query)
    {
        /** @var Query $countQuery */
        $countQuery = clone $query;

        $countQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('DoctrineExtensions\Paginate\CountSqlWalker'));
        $countQuery->setFirstResult(null)->setMaxResults(null);

        return $countQuery->getSingleScalarResult();
    }
}

It clones the query, resets the limit clause first and max results and registers the CountSqlWalker custom tree walker which will modify the AST to execute a count query. The walkers implementation is:

クエリを複製し、最初に制限句と最大結果をリセットし、カウント クエリを実行するように AST を変更する CountSqlWalker カスタム ツリー ウォーカーを登録します。ウォーカーの実装は次のとおりです。

<?php
class CountSqlWalker extends TreeWalkerAdapter
{
    /**
     * Walks down a SelectStatement AST node, thereby generating the appropriate SQL.
     *
     * @return string The SQL.
     */
    public function walkSelectStatement(SelectStatement $AST)
    {
        $parent = null;
        $parentName = null;
        foreach ($this->_getQueryComponents() as $dqlAlias => $qComp) {
            if ($qComp['parent'] === null && $qComp['nestingLevel'] == 0) {
                $parent = $qComp;
                $parentName = $dqlAlias;
                break;
            }
        }

        $pathExpression = new PathExpression(
            PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $parentName,
            $parent['metadata']->getSingleIdentifierFieldName()
        );
        $pathExpression->type = PathExpression::TYPE_STATE_FIELD;

        $AST->selectClause->selectExpressions = array(
            new SelectExpression(
                new AggregateExpression('count', $pathExpression, true), null
            )
        );
    }
}

This will delete any given select expressions and replace them with a distinct count query for the root entities primary key. This will only work if your entity has only one identifier field (composite keys won’t work).

これにより、指定された選択式が削除され、ルート エンティティの主キーの個別のカウント クエリに置き換えられます。これは、エンティティに識別子フィールドが 1 つしかない場合にのみ機能します (複合キーは機能しません)。

Modify the Output Walker to generate Vendor specific SQL

Most RMDBS have vendor-specific features for optimizing select query execution plans. You can write your own output walker to introduce certain keywords using the Query Hint API. A query hint can be set via Query::setHint($name, $value) as shown in the previous example with the HINT_CUSTOM_TREE_WALKERS query hint.

ほとんどの RMDBS には、selectquery 実行プランを最適化するためのベンダー固有の機能があります。 Query Hint API を使用して、特定のキーワードを導入する独自の出力ウォーカーを作成できます。前の例の HINT_CUSTOM_TREE_WALKERS クエリ ヒントで示したように、Query::setHint($name, $value) を介してクエリ ヒントを設定できます。

We will implement a custom Output Walker that allows to specify the SQL_NO_CACHE query hint.

SQL_NO_CACHE クエリ ヒントを指定できるカスタム Output Walker を実装します。

<?php
$dql = "SELECT p, c, a FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...";
$query = $m->createQuery($dql);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'DoctrineExtensions\Query\MysqlWalker');
$query->setHint("mysqlWalker.sqlNoCache", true);
$results = $query->getResult();

Our MysqlWalker will extend the default SqlWalker. We will modify the generation of the SELECT clause, adding the SQL_NO_CACHE on those queries that need it:

MysqlWalker はデフォルトの SqlWalker を拡張します。 SELECT 句の生成を変更し、必要なクエリに SQL_NO_CACHE を追加します。

<?php
class MysqlWalker extends SqlWalker
{
     /**
     * Walks down a SelectClause AST node, thereby generating the appropriate SQL.
     *
     * @param $selectClause
     * @return string The SQL.
     */
    public function walkSelectClause($selectClause)
    {
        $sql = parent::walkSelectClause($selectClause);

        if ($this->getQuery()->getHint('mysqlWalker.sqlNoCache') === true) {
            if ($selectClause->isDistinct) {
                $sql = str_replace('SELECT DISTINCT', 'SELECT DISTINCT SQL_NO_CACHE', $sql);
            } else {
                $sql = str_replace('SELECT', 'SELECT SQL_NO_CACHE', $sql);
            }
        }

        return $sql;
    }
}

Writing extensions to the Output Walker requires a very deep understanding of the DQL Parser and Walkers, but may offer your huge benefits with using vendor specific features. This would still allow you write DQL queries instead of NativeQueries to make use of vendor specific features.

Output Walker の拡張機能を作成するには、DQL パーサーと Walker を深く理解する必要がありますが、ベンダー固有の機能を使用すると大きなメリットが得られる場合があります。これにより、NativeQueries の代わりに DQL クエリを記述して、ベンダー固有の機能を利用できます。

Table Of Contents

Previous topic

Persisting the Decorator Pattern

デコレータ パターンの永続化

Next topic

DQL User Defined Functions

DQL ユーザー定義関数

This Page

Fork me on GitHub