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

时间:2015-05-14 16:05:24

标签: cakephp orm associations cakephp-3.0 query-builder

我有一个模型,文章,其中有很多摘要。我想加载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'])查找文章byCategory,只有这会杀死我的SQL。它死于可怕的死亡:

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

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

修改

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

1 个答案:

答案 0 :(得分: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

选择策略并反向排序

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

这将选择所有相关的摘要,然后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个关联,但是我现在太忙了以进一步追求这个...只是根据ROW_NUMBER()仿真类似,将MySQL特定的自定义关联放在一起进行测试到 MySQL select top X records for each individual in table

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