DQL User Defined Functions

By default DQL supports a limited subset of all the vendor-specific SQL functions common between all the vendors. However in many cases once you have decided on a specific database vendor, you will never change it during the life of your project. This decision for a specific vendor potentially allows you to make use of powerful SQL features that are unique to the vendor.

デフォルトでは、DQL は、すべてのベンダー間で共通のすべてのベンダー固有の SQL 関数の限定されたサブセットをサポートします。ただし、多くの場合、特定のデータベース ベンダーを決定すると、プロジェクトの存続期間中にそれを変更することはありません。特定のベンダーに対するこの決定により、ベンダー固有の強力な SQL 機能を利用できる可能性があります。

It is worth to mention that Doctrine ORM also allows you to handwrite your SQL instead of extending the DQL parser. Extending DQL is sort of an advanced extension point. You can map arbitrary SQL to your objects and gain access to vendor specific functionalities using the EntityManager#createNativeQuery() API as described in the Native Query chapter.

Doctrine ORM では、DQL パーサーを拡張する代わりに SQL を手書きすることもできることに注意してください。 DQL の拡張は、一種の高度な拡張ポイントです。ネイティブ クエリの章で説明されているように、EntityManager#createNativeQuery() API を使用して、オブジェクトに任意の SQL をマップし、ベンダー固有の機能にアクセスできます。

The DQL Parser has hooks to register functions that can then be used in your DQL queries and transformed into SQL, allowing to extend Doctrines Query capabilities to the vendors strength. This post explains the User-Defined Functions API (UDF) of the Dql Parser and shows some examples to give you some hints how you would extend DQL.

DQL パーサーには、DQL クエリで使用して SQL に変換できる関数を登録するためのフックがあり、Doctrines Query 機能をベンダーの強みまで拡張することができます。この投稿では、DqlParser のユーザー定義関数 API (UDF) について説明し、DQL を拡張する方法のヒントを示すいくつかの例を示します。

There are three types of functions in DQL, those that return a numerical value, those that return a string and those that return a Date. Your custom method has to be registered as either one of those. The return type information is used by the DQL parser to check possible syntax errors during the parsing process, for example using a string function return value in a math expression.

DQL には、数値を返す関数、文字列を返す関数、および aDate を返す関数の 3 種類の関数があります。カスタム メソッドは、これらのいずれかとして登録する必要があります。戻り値の型情報は、DQL パーサーによって使用され、解析プロセス中に発生する可能性のある構文エラーをチェックします。たとえば、数式で文字列関数の戻り値を使用する場合などです。

Registering your own DQL functions

You can register your functions adding them to the ORM configuration:

関数を ORM 構成に追加して登録できます。

<?php
$config = new \Doctrine\ORM\Configuration();
$config->addCustomStringFunction($name, $class);
$config->addCustomNumericFunction($name, $class);
$config->addCustomDatetimeFunction($name, $class);

$em = new EntityManager($connection, $config);

The $name is the name the function will be referred to in the DQL query. $class is a string of a class-name which has to extend Doctrine\ORM\Query\Node\FunctionNode. This is a class that offers all the necessary API and methods to implement a UDF.

$name は、関数が DQL クエリで参照される名前です。 $class は、Doctrine\ORM\Query\Node\FunctionNode を拡張するクラス名の文字列です。これは、UDF を実装するために必要なすべての API とメソッドを提供するクラスです。

Instead of providing the function class name, you can also provide a callable that returns the function object:

関数クラス名を提供する代わりに、関数オブジェクトを返す callable を提供することもできます。

<?php
$config = new \Doctrine\ORM\Configuration();
$config->addCustomStringFunction($name, function () {
    return new MyCustomFunction();
});

In this post we will implement some MySql specific Date calculation methods, which are quite handy in my opinion:

この投稿では、MySql 固有の Date 計算メソッドをいくつか実装します。これは、私の意見では非常に便利です。

Date Diff

Mysql’s DateDiff function takes two dates as argument and calculates the difference in days with date1-date2.

Mysql の DateDiff 関数は、引数として 2 つの日付を取り、date1-date2 で日数の差を計算します。

The DQL parser is a top-down recursive descent parser to generate the Abstract-Syntax Tree (AST) and uses a TreeWalker approach to generate the appropriate SQL from the AST. This makes reading the Parser/TreeWalker code manageable in a finite amount of time.

DQL パーサーは、Abstract-Syntax Tree (AST) を生成するトップダウン再帰降下パーサーであり、TreeWalker アプローチを使用して AST から適切な SQL を生成します。これにより、Parser/TreeWalker コードの読み取りを限られた時間で管理できるようになります。

The FunctionNode class I referred to earlier requires you to implement two methods, one for the parsing process (obviously) called parse and one for the TreeWalker process called getSql(). I show you the code for the DateDiff method and discuss it step by step:

前に説明した FunctionNode クラスでは、2 つのメソッドを実装する必要があります。1 つは (明らかに) parse と呼ばれる解析プロセス用で、もう 1 つは getSql() と呼ばれる TreeWalker プロセス用です。 DateDiff メソッドのコードを示し、順を追って説明します。

<?php
/**
 * DateDiffFunction ::= "DATEDIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
 */
class DateDiff extends FunctionNode
{
    // (1)
    public $firstDateExpression = null;
    public $secondDateExpression = null;

    public function parse(\Doctrine\ORM\Query\Parser $parser)
    {
        $parser->match(Lexer::T_IDENTIFIER); // (2)
        $parser->match(Lexer::T_OPEN_PARENTHESIS); // (3)
        $this->firstDateExpression = $parser->ArithmeticPrimary(); // (4)
        $parser->match(Lexer::T_COMMA); // (5)
        $this->secondDateExpression = $parser->ArithmeticPrimary(); // (6)
        $parser->match(Lexer::T_CLOSE_PARENTHESIS); // (3)
    }

    public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
    {
        return 'DATEDIFF(' .
            $this->firstDateExpression->dispatch($sqlWalker) . ', ' .
            $this->secondDateExpression->dispatch($sqlWalker) .
        ')'; // (7)
    }
}

The Parsing process of the DATEDIFF function is going to find two expressions the date1 and the date2 values, whose AST Node representations will be saved in the variables of the DateDiff FunctionNode instance at (1).

DATEDIFF 関数の解析プロセスは、date1 値と date2 値の 2 つの式を見つけようとしています。その AST ノード表現は、(1) の DateDiffFunctionNode インスタンスの変数に保存されます。

The parse() method has to cut the function call “DATEDIFF” and its argument into pieces. Since the parser detects the function using a lookahead the T_IDENTIFIER of the function name has to be taken from the stack (2), followed by a detection of the arguments in (4)-(6). The opening and closing parenthesis have to be detected also. This happens during the Parsing process and leads to the generation of a DateDiff FunctionNode somewhere in the AST of the dql statement.

parse() メソッドは、関数呼び出し「DATEDIFF」とその引数を分割する必要があります。パーサーは先読みを使用して関数を検出するため、関数名の T_IDENTIFIER をスタックから取得する必要があり (2)、続いて (4) ~ (6) で引数を検出します。開き括弧と閉じ括弧も検出する必要があります。これは解析プロセス中に発生し、dql ステートメントの AST のどこかに DateDiff FunctionNode が生成されます。

The ArithmeticPrimary method call is the most common denominator of valid EBNF tokens taken from the DQL EBNF grammar that matches our requirements for valid input into the DateDiff Dql function. Picking the right tokens for your methods is a tricky business, but the EBNF grammar is pretty helpful finding it, as is looking at the Parser source code.

ArithmeticPrimary メソッド呼び出しは、DateDiff Dql 関数への有効な入力の要件に一致する DQL EBNF 文法から取得された有効な EBNF トークンの最も一般的な分母です。メソッドに適切なトークンを選択するのはトリッキーな作業ですが、パーサーのソース コードを見ているように、EBNF 文法はそれを見つけるのに非常に役立ちます。

Now in the TreeWalker process we have to pick up this node and generate SQL from it, which apparently is quite easy looking at the code in (7). Since we don’t know which type of AST Node the first and second Date expression are we are just dispatching them back to the SQL Walker to generate SQL from and then wrap our DATEDIFF function call around this output.

TreeWalker プロセスでは、このノードを取得してそこから SQL を生成する必要があります。(7) のコードを見ると、これは明らかに非常に簡単です。 1 番目と 2 番目の Date 式がどのタイプの AST ノードであるかがわからないため、それらを SQL Walker にディスパッチして戻して SQL を生成し、DATEDIFF 関数呼び出しをこの出力にラップします。

Now registering this DateDiff FunctionNode with the ORM using:

以下を使用して、この DateDiff FunctionNode を ORM に登録します。

<?php
$config = new \Doctrine\ORM\Configuration();
$config->addCustomStringFunction('DATEDIFF', 'DoctrineExtensions\Query\MySql\DateDiff');

We can do fancy stuff like:

次のような派手なことを行うことができます。

SELECT p FROM DoctrineExtensions\Query\BlogPost p WHERE DATEDIFF(CURRENT_TIME(), p.created) < 7

Date Add

Often useful it the ability to do some simple date calculations in your DQL query using MySql’s DATE_ADD function.

MySql の DATE_ADD 関数を使用して、DQL クエリでいくつかの簡単な日付計算を実行できると便利なことがよくあります。

I’ll skip the blah and show the code for this function:

何とかスキップして、この関数のコードを示します。

<?php
/**
 * DateAddFunction ::=
 *     "DATE_ADD" "(" ArithmeticPrimary ", INTERVAL" ArithmeticPrimary Identifier ")"
 */
class DateAdd extends FunctionNode
{
    public $firstDateExpression = null;
    public $intervalExpression = null;
    public $unit = null;

    public function parse(\Doctrine\ORM\Query\Parser $parser)
    {
        $parser->match(Lexer::T_IDENTIFIER);
        $parser->match(Lexer::T_OPEN_PARENTHESIS);

        $this->firstDateExpression = $parser->ArithmeticPrimary();

        $parser->match(Lexer::T_COMMA);
        $parser->match(Lexer::T_IDENTIFIER);

        $this->intervalExpression = $parser->ArithmeticPrimary();

        $parser->match(Lexer::T_IDENTIFIER);

        /** @var Lexer $lexer */
        $lexer = $parser->getLexer();
        $this->unit = $lexer->token['value'];

        $parser->match(Lexer::T_CLOSE_PARENTHESIS);
    }

    public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
    {
        return 'DATE_ADD(' .
            $this->firstDateExpression->dispatch($sqlWalker) . ', INTERVAL ' .
            $this->intervalExpression->dispatch($sqlWalker) . ' ' . $this->unit .
        ')';
    }
}

The only difference compared to the DATEDIFF here is, we additionally need the Lexer to access the value of the T_IDENTIFIER token for the Date Interval unit, for example the MONTH in:

ここでの DATEDIFF との唯一の違いは、日付間隔単位の T_IDENTIFIER トークンの値にアクセスするためにレクサーがさらに必要になることです。

SELECT p FROM DoctrineExtensions\Query\BlogPost p WHERE DATE_ADD(CURRENT_TIME(), INTERVAL 4 MONTH) > p.created

The above method now only supports the specification using INTERVAL, to also allow a real date in DATE_ADD we need to add some decision logic to the parsing process (makes up for a nice exercise).

上記のメソッドは現在、INTERVAL を使用した仕様のみをサポートしています。DATE_ADD で実際の日付も許可するには、解析プロセスにいくつかの決定ロジックを追加する必要があります (ナイスエクササイズを補う)。

Now as you see, the Parsing process doesn’t catch all the possible SQL errors, here we don’t match for all the valid inputs for the interval unit. However where necessary we rely on the database vendors SQL parser to show us further errors in the parsing process, for example if the Unit would not be one of the supported values by MySql.

ご覧のとおり、解析プロセスは考えられるすべての SQL エラーをキャッチしているわけではありません。ここでは、interval ユニットのすべての有効な入力が一致していません。ただし、必要に応じて、データベース ベンダーの SQL パーサーを使用して、解析プロセスでさらにエラーを表示します。たとえば、Unit が MySql でサポートされている値の 1 つでない場合などです。

Conclusion

Now that you all know how you can implement vendor specific SQL functionalities in DQL, we would be excited to see user extensions that add vendor specific function packages, for example more math functions, XML + GIS Support, Hashing functions and so on.

ベンダー固有の SQL 機能を DQL に実装する方法がわかったので、数学関数、XML + GIS サポート、ハッシュ関数など、ベンダー固有の関数パッケージを追加するユーザー拡張機能を期待しています。

For ORM we will come with the current set of functions, however for a future version we will re-evaluate if we can abstract even more vendor sql functions and extend the DQL languages scope.

ORM には現在の関数セットが付属していますが、将来のバージョンでは、さらに多くのベンダー SQL 関数を抽象化し、DQL 言語の範囲を拡張できるかどうかを再評価します。

Code for this Extension to DQL and other Doctrine Extensions can be found in the GitHub DoctrineExtensions repository.

この DQL 拡張機能およびその他の Doctrine 拡張機能のコードは、GitHub DoctrineExtensions リポジトリで見つけることができます。

Table Of Contents

Previous topic

Extending DQL in Doctrine ORM: Custom AST Walkers

Doctrine ORM での DQL の拡張: カスタム AST ウォーカー

Next topic

Implementing ArrayAccess for Domain Objects

ドメイン オブジェクトの ArrayAccess の実装

This Page

Fork me on GitHub