如何修改 CakePHP 3 中的 UNION 查询?

How do you modify a UNION query in CakePHP 3?

我想在 CakePHP 3.0.0 中对联合查询进行分页。通过使用 custom finder,我几乎可以 完美地工作,但是我找不到任何方法让 limitoffset 应用于联合,而不是任何一个子查询。

换句话说,这段代码:

$articlesQuery = $articles->find('all');
$commentsQuery = $comments->find('all');
$unionQuery = $articlesQuery->unionAll($commentsQuery);
$unionQuery->limit(7)->offset(7); // nevermind the weirdness of applying this manually

生成此查询:

(SELECT {article stuff} ORDER BY created DESC LIMIT 7 OFFSET 7)
UNION ALL 
(SELECT {comment stuff}) 

而不是我想要的,是这样的:

(SELECT {article stuff})
UNION ALL 
(SELECT {comment stuff})
ORDER BY created DESC LIMIT 7 OFFSET 7

可以像这样手动构造正确的查询字符串:

$unionQuery = $articlesQuery->unionAll($commentsQuery);
$sql = $unionQuery->sql();
$sql = "($sql) ORDER BY created DESC LIMIT 7 OFFSET 7";

但我的自定义查找器方法需要 return 一个 \Cake\Database\Query 对象,而不是字符串。

所以,

注意: 有 a closed issue 描述了与此类似的内容(除了使用 paginate($unionQuery)),但没有关于如何克服该问题的建议。

对每个子查询应用限制和偏移量?

scrowler 友善地建议了这个选项,但我认为它行不通。如果 limit 设置为 5 并且完整的结果集将是这样的:

Article 9     --|
Article 8       |
Article 7       -- Page One
Article 6       |
Article 5     --|

Article 4     --|
Comment 123     |
Article 3       -- Here be dragons
Comment 122     |
Comment 121   --|
...

然后查询第 1 页就可以了,因为(前五篇文章)+(前五篇评论),按日期手动排序,并修剪到组合结果的前五篇,结果是文章 1 -5.

但第 2 页将无法正常工作,因为 offset 5 将同时应用于文章和评论,这意味着前 5 条评论(不是' t 包含在第 1 页),永远不会出现在结果中。

能够在 unionAll() 返回的查询上 直接 应用这些子句是不可能的 AFAIK,它需要对 API 进行更改让编译器知道把 SQL 放在哪里,通过选项,一种新型的查询对象等等。

Query::epilog() 拯救

幸运的是,可以使用 Query::epilog() 将 SQL 附加到查询,因为它是原始 SQL 片段

$unionQuery->epilog('ORDER BY created DESC LIMIT 7 OFFSET 7');

或查询表达式

$unionQuery->epilog(
    $connection->newQuery()->order(['created' => 'DESC'])->limit(7)->offset(7)
);

这应该会为您提供所需的查询。

需要注意的是,根据文档 Query::epilog() 需要一个字符串,或者一个 \Cake\Database\Expression\QueryExpression 实例形式的具体 \Cake\Database\ExpressionInterface 实现,而不仅仅是 any ExpressionInterface 实现,所以理论上后一个示例是无效的,即使查询编译器适用于任何 ExpressionInterface 实现。

使用子查询

也可以将联合查询用作子查询,这将使使用分页组件的上下文中的事情变得更容易,因为除了构建和注入子查询之外,您无需处理任何其他事情,因为分页器组件可以简单地在主查询上应用 order/limit/offset。

/* @var $connection \Cake\Database\Connection */
$connection = $articles->connection();

$articlesQuery = $connection
    ->newQuery()
    ->select(['*'])
    ->from('articles');
    
$commentsQuery = $connection
    ->newQuery()
    ->select(['*'])
    ->from('comments');
    
$unionQuery = $articlesQuery->unionAll($commentsQuery);

$paginatableQuery = $articles
    ->find()
    ->from([$articles->alias() => $unionQuery]);

这当然也可以移到取景器中。