如何限制每个记录/组的包含关联?

问题描述 投票:9回答:1

我有一个模型,文章,其中有很多摘要。我想加载10篇最新文章,并为每篇文章加载具有最高分数的摘要。我的功能看起来像这样:

public function getArticles($category, $viewName) {
            $subArticles = $this->Articles->findByCategory($category)->contain([
                    'Abstracts' => function ($q) {
                            return $q
                                    ->select(['body', 'points', 'article_id'])
                                    ->where(['Abstracts.approved' => true])
                                    ->limit(10)
                                    ->order(['Abstracts.points' => 'DESC']);
                    }
            ])
            ->limit(10)
            ->order(['Articles.created' => 'DESC']) ;
            $this->set( $viewName . 'Articles', $subArticles );
    }

我得到的结果不是我想要的。通过SQL,首先CakePHP获取了类别中所有内容的articles.id(很好)。然后,CakePHP进入Abstracts表,使用那10篇文章。它刚刚找到,并要求获得最高票数的10篇摘要(属于那些文章)。

问题是我希望每篇文章都有1篇摘要,而不是属于该类别任何文章的10篇摘要。我怎样才能解决这个问题?谢谢!

编辑

ndm建议这是Using limit() on contained model的副本,所以我尝试了那里的解决方案。也就是说,我将此添加到我的模型中:

 $this->hasOne('TopAbstract', [
            'className' => 'Abstracts',
            'foreignKey' => 'abstract_id',
            'strategy' => 'select',
            'sort' => ['TopAbstract.points' => 'DESC'],
            'conditions' => function ($e, $query) {
            $query->limit(1);
            return $e;
    } ]);

然后我尝试使用contains(['TopAbstract'])找到Articles byCategory,只有这会杀死我的SQL。它死于可怕的死亡:

Error: SQLSTATE[HY000]: General error: 1 near ")": syntax error

Debug甚至没有显示杀死它的查询,所以我不确定如何调试这个?

编辑

稍微和自己说话,但错误肯定是在hasOne的'条件'部分。我把它拿出来,它工作正常。无法找到这个应该如何看待互联网的例子..任何人都有任何想法?

cakephp orm associations cakephp-3.0 query-builder
1个回答
25
投票

您正在寻找的是问题的解决方案。您没有提到任何特定的RDBMS,但仍然可以看到http://dev.mysql.com/doc/refman/5.6/en/example-maximum-column-group-row.html

所以让我们试一试,这里有三个可以应用于关联级别的选项(定义条件也可以移动到自定义查找程序中),但是你可能会认为它们不是那么“直截了当”。


对于特定的HasMany,滚动一路向下!


选择策略 - 在分组的最大值子查询上使用连接

$this->hasOne('TopAbstracts', [
    'className' => 'Abstracts',
    'strategy' => 'select',
    'conditions' => function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) {
        $query->innerJoin(
            [
                'AbstractsFilter' => $query
                    ->connection()
                    ->newQuery()
                    ->select(['article_id', 'points' => $query->func()->max('points')])
                    ->from('abstracts')
                    ->group('article_id')
            ],
            [
                'TopAbstracts.article_id = AbstractsFilter.article_id',
                'TopAbstracts.points = AbstractsFilter.points'
            ]
        );
        return [];
    }
]);

这将通过基于最大点的连接查询选择顶部摘要,它看起来像

SELECT
    TopAbstracts.id AS `TopAbstracts__id`, ...
FROM
    abstracts TopAbstracts
INNER JOIN (
        SELECT
            article_id, (MAX(points)) AS `points`
        FROM
            abstracts
        GROUP BY
            article_id
    )
    AbstractsFilter ON (
        TopAbstracts.article_id = AbstractsFilter.article_id
        AND
        TopAbstracts.points = AbstractsFilter.points
    )
WHERE
    TopAbstracts.article_id in (1,2,3,4,5,6,7,8, ...)

选择策略 - 使用左自联接过滤

$this->hasOne('TopAbstracts', [
    'className' => 'Abstracts',
    'strategy' => 'select',
    'conditions' => function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) {
        $query->leftJoin(
            ['AbstractsFilter' => 'abstracts'],
            [
                'TopAbstracts.article_id = AbstractsFilter.article_id',
                'TopAbstracts.points < AbstractsFilter.points'
            ]);
        return $exp->add(['AbstractsFilter.id IS NULL']);
    }
]);

这将使用基于没有a.points < b.points的行过滤的自联接,它看起来像

SELECT
    TopAbstracts.id AS `TopAbstracts__id`, ...
FROM 
    abstracts TopAbstracts
LEFT JOIN
    abstracts AbstractsFilter ON (
        TopAbstracts.article_id = AbstractsFilter.article_id
        AND
        TopAbstracts.points < AbstractsFilter.points
    )
WHERE
    (AbstractsFilter.id IS NULL AND TopAbstracts.article_id in (1,2,3,4,5,6,7,8, ...))

加入策略 - 使用子查询进行连接条件

$this->hasOne('TopAbstracts', [
    'className' => 'Abstracts',
    'foreignKey' => false,
    'conditions' => function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) {
        $subquery = $query
            ->connection()
            ->newQuery()
            ->select(['SubTopAbstracts.id'])
            ->from(['SubTopAbstracts' => 'abstracts'])
            ->where(['Articles.id = SubTopAbstracts.article_id'])
            ->order(['SubTopAbstracts.points' => 'DESC'])
            ->limit(1);

        return $exp->add(['TopAbstracts.id' => $subquery]);
    }
]);

这将使用相关子查询,该子查询使用具有简单排序的相当具体的选择,并限制选择最高注释。请注意,foreignKey选项设置为false,以避免将其他Articles.id = TopAbstracts.article_id条件编译到连接条件中。

查询看起来像

SELECT
    Articles.id AS `Articles__id`, ... ,
    TopAbstracts.id AS `TopAbstracts__id`, ...
FROM
    articles Articles
LEFT JOIN
    abstracts TopAbstracts ON (
        TopAbstracts.id = (
            SELECT
                SubTopAbstracts.id
            FROM
                abstracts SubTopAbstracts
            WHERE
                Articles.id = SubTopAbstracts.article_id
            ORDER BY
                SubTopAbstracts.points DESC
            LIMIT
                1
        )
    )

所有这3个选项都会查询并注入记录而没有任何hackery,它只是不是非常“直截了当”。


手动方法

为了完整起见,当然总是可以手动加载关联记录并适当地格式化结果,例如使用结果格式化程序,例如参见CakePHP Entity contain without foreign key


Select strategy and reversed ordering

仅供参考,我最初偶然发现了一个奇怪的解决方案。真的不应该使用这个!

这将选择所有相关的摘要,然后ORM将迭代它们,并且每篇文章选择具有匹配的article_id值的第一个。因此在理论上,当在points上订购时,ORM应该选择最多点的那个。

虽然我本来期望这个开箱即用,但似乎ORM以相反的顺序迭代结果,这将导致选择错误的行。为了使这个工作,查询需要使用通常需要使用的相反顺序,即ASC而不是DESC

$this->hasOne('TopAbstracts', [
    'className' => 'Abstracts',
    'foreignKey' => 'abstract_id',
    'strategy' => 'select',
    'conditions' => function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) {
        $query->order(['TopAbstracts.points' => 'ASC']);
        return [];
    }
]);

此函数还需要返回一个空数组而不是链接答案中显示的表达式,因为这将导致编译无效的SQL。这两种行为,反向顺序迭代和无效的SQL可能都是错误。

虽然这会起作用,但它总会选择所有相关的摘要,而不仅仅是最重要的摘要,这些摘要可能被认为效率很低,看起来像

SELECT
    Articles.id AS `Articles__id`, ...
FROM
    articles Articles
SELECT
    TopAbstracts.id AS `TopAbstracts__id`, ...
FROM
    abstracts TopAbstracts
WHERE
    TopAbstracts.article_id in (1,2,3,4,5,6,7,8, ...)
ORDER BY
    TopAbstracts.points ASC

HasMany协会

我尝试了HasMany协会,但是我现在太忙了以进一步追求这个...只是为了测试目的而组建一个MySQL特定的自定义关联,基于类似于ROW_NUMBER()MySQL select top X records for each individual in table仿真。

如果有人有兴趣,请查看https://gist.github.com/ndm2/039da4009df1c5bf1c262583603f8298

© www.soinside.com 2019 - 2024. All rights reserved.