cbcc693e36
git-subtree-dir: docs git-subtree-mainline:271bd37ad3
git-subtree-split:8fcf2d4501
218 lines
7.8 KiB
ReStructuredText
218 lines
7.8 KiB
ReStructuredText
Extending DQL in Doctrine 2: Custom AST Walkers
|
|
===============================================
|
|
|
|
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
|
|
|
|
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.
|
|
|
|
In Doctrine 1 the DQL language was not implemented using a real
|
|
parser. This made modifications of the DQL by the user impossible.
|
|
Doctrine 2 in contrast has a real parser for the DQL language,
|
|
which transforms the DQL statement into an
|
|
`Abstract Syntax Tree <http://en.wikipedia.org/wiki/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.
|
|
|
|
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.
|
|
|
|
There are two types of custom tree walkers that you can hook into
|
|
the DQL parser:
|
|
|
|
|
|
- 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.
|
|
- 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.
|
|
|
|
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:
|
|
|
|
|
|
- Modify the AST to generate a Count Query to be used with a
|
|
paginator for any given DQL query.
|
|
- Modify the Output Walker to generate vendor-specific SQL
|
|
(instead of ANSI).
|
|
- Modify the AST to add additional where clauses for specific
|
|
entities (example ACL, country-specific content...)
|
|
- Modify the Output walker to pretty print the SQL for debugging
|
|
purposes.
|
|
|
|
In this cookbook-entry I will show examples on the first two
|
|
points. There are probably much more use-cases.
|
|
|
|
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:
|
|
|
|
.. code-block:: sql
|
|
|
|
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 its 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:
|
|
|
|
.. code-block:: sql
|
|
|
|
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. Lets see how the
|
|
API would look for this use-case:
|
|
|
|
.. code-block:: php
|
|
|
|
<?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:
|
|
|
|
.. code-block:: php
|
|
|
|
<?php
|
|
class Paginate
|
|
{
|
|
static public function count(Query $query)
|
|
{
|
|
/* @var $countQuery Query */
|
|
$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`` customer tree walker which
|
|
will modify the AST to execute a count query. The walkers
|
|
implementation is:
|
|
|
|
.. code-block:: php
|
|
|
|
<?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).
|
|
|
|
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.
|
|
|
|
We will implement a custom Output Walker that allows to specify the
|
|
SQL\_NO\_CACHE query hint.
|
|
|
|
.. code-block:: php
|
|
|
|
<?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:
|
|
|
|
.. code-block:: php
|
|
|
|
<?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.
|
|
|