Skip to content

自定义函数

twosee edited this page Mar 4, 2024 · 1 revision

本章节翻译自DQL User Defined Functions,并对部分内容进行了调整。

DQL解析器提供了一些钩子,用于注册函数。这些函数可以在你的DQL查询中使用,并被转换为SQL,从而增强Doctrine查询的功能。本文将解释DQL解析器的用户定义函数API(UDF),并提供一些示例,以帮助你更好地理解如何扩展DQL。

在DQL中,有三种函数类型:返回数值、返回字符串和返回日期。你的自定义方法需要注册为其中之一。返回类型信息在DQL解析器中用于检查解析过程中可能的语法错误,例如在数学表达式中使用字符串函数的返回值。

总的来说,通过这些函数的注册,你可以更灵活地定制DQL查询,使其适应不同的需求和数据库操作。

注册函数

你可以通过配置文件来注册函数:

<?php

declare(strict_types=1);

return [
    'default' => [
        'configuration' => [
            ......
            'functions' => [
                [
                    'name' => 'test_substring',
                    'className' => SubstringFunction::class,
                    'type' => 'string',
                ],
                [
                    'name' => 'test_rand',
                    'className' => RandFunction::class,
                    'type' => 'numeric',
                ],
                [
                    'name' => 'test_now',
                    'className' => NowFunction::class,
                    'type' => 'datetime',
                ],
            ],
        ],
        'connection' => [
            ......
        ],
    ],
];

在上述配置文件的 functions 部分,我们分别配置了三种类型的函数。

name是函数在DQL查询中的引用名称。class是一个字符串,表示一个类名,该类必须继承自Doctrine\ORM\Query\Node\FunctionNode。这个类提供了实现用户定义函数(UDF)所需的所有API和方法。

在本篇文章中,我们将实现一些特定于MySQL的日期计算方法:

Date Diff

Mysql的DateDiff函数 接受两个日期作为参数,并计算日期之间的天数差date1-date2

DQL解析器采用自顶向下递归下降(top-down recursive descent)的方式生成抽象语法树(AST),并通过TreeWalker方法从AST生成相应的SQL。这种设计使得我们能够在有限的时间内相对轻松地阅读Parser/TreeWalker的代码。

之前提到的FunctionNode类要求你实现两个方法,一个用于解析过程(parse方法),另一个用于TreeWalker过程的getSql方法。接下来,我将展示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(TokenType::T_IDENTIFIER); // (2)
        $parser->match(TokenType::T_OPEN_PARENTHESIS); // (3)
        $this->firstDateExpression = $parser->ArithmeticPrimary(); // (4)
        $parser->match(TokenType::T_COMMA); // (5)
        $this->secondDateExpression = $parser->ArithmeticPrimary(); // (6)
        $parser->match(TokenType::T_CLOSE_PARENTHESIS); // (3)
    }

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

DATEDIFF函数的解析过程会找到两个表达式,即date1和date2的值,并将它们的AST节点表示保存在DateDiff FunctionNode实例的变量中(1)。

parse()方法必须将函数调用DATEDIFF及其参数切分为片段。由于解析器使用先行符进行函数检测,函数名的T_IDENTIFIER必须从堆栈中获取(2),然后在(4)-(6)中检测参数。同时,还需检测开括号和闭括号。这些都发生在解析过程中,并最终在dql语句的AST中生成一个DateDiff FunctionNode。

调用ArithmeticPrimary方法是因为它代表了在DQL语法中最基本、最通用的令牌。这些令牌符合我们对于DateDiff DQL函数输入的要求。选择正确的令牌对于这些方法来说可能是有挑战性的,但EBNF语法(DQL EBNF语法)提供了很好的指导,就好像查看解析器源代码也能提供一些启示。

在TreeWalker的过程中,我们需要捕获这个节点并从中生成SQL,从代码中(7)看起来这似乎相当容易。由于我们不知道第一个和第二个日期表达式的AST节点类型,因此我们只需将它们分发回SQL Walker以生成SQL,然后将DATEDIFF函数调用包装在这个输出周围。

在配置文件中注册这个 DateDiff FunctionNode:

<?php

declare(strict_types=1);

return [
    'default' => [
        'configuration' => [
            ......
            'functions' => [
                [
                    'name' => 'datediff',
                    'className' => DateDiff::class,
                    'type' => 'string',
                ]
            ],
        ],
        'connection' => [
            ......
        ],
    ],
];

现在我们可以尝试执行以下DQL:

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