【问题标题】:How to limit contained associations per record/group?如何限制每个记录/组包含的关联?
【发布时间】:2015-07-26 08:14:40
【问题描述】:

我有一个模型,文章,它有许多摘要。我想加载 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 正在获取类别中所有内容的 article.id(很好)。然后,CakePHP 进入 Abstracts 表,使用它刚刚找到的那 10 个 article.id,并请求获得最高票数的 10 个 Abstracts(属于这些 Articles)。

问题是我希望每篇文章有 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;
    } ]);

然后我尝试使用包含(['TopAbstract']),按类别查找文章,但这会杀死我的 SQL。它死得很惨:

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

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

编辑

与自己交谈了一下​​,但错误肯定在 hasOne 的“条件”部分。我把它拿出来,它工作正常。在互联网上找不到这应该如何看待的示例.. 有人知道吗?

【问题讨论】:

  • 是的,在那里尝试了建议的解决方案,但无济于事。将编辑我的问题以反映进度/持续失败:-(
  • 也就是说,目前这个问题没有解决办法吗?嗯.. 似乎是一个很常见的问题,我有对吗?
  • 我已经删除了我之前的评论,因为我认为还有更多错误。似乎这根本不会那样工作,因为select 策略只会导致一个额外的查询,并且应用了限制,它总是只有一个结果。恐怕答案可能是,或者已经变得不正确,或者可能只是另一个错误,不确定。当不应用限制而只应用订单时,它有点工作,但订单将被颠倒,即ASC 订单将选择结果,就好像它是 DESC 订单一样,反之亦然。
  • 这很奇怪,然后可能是 SQL 编译器错误导致错误(例如,如果您返回一个空数组,它会起作用,$e 是一个“空”QueryExpression带有AND 连词的实例),您可能想要report this over at GitHub

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


【解决方案1】:

您正在寻找的是 问题的解决方案。您没有提及任何特定的 RDBMS,但也请参阅 http://dev.mysql.com/doc/refman/5.6/en/example-maximum-column-group-row.html

图书馆解决方案

对于那些有点冒险精神的人,我开发了一些自定义关联,它们透明地集成到 ORM 层中,并允许对 hasManybelongsToMany 关系的每个组进行基本限制:https://github.com/icings/partitionable

使用它们,问题的解决方案是建立这样的关联:

$this
    ->partitionableHasMany('TopAbstracts')
    ->setClassName('Abstracts')
    ->setLimit(1)
    ->setSort([
        'Abstracts.points' => 'DESC',
        'Abstracts.id' => 'ASC',
    ]);

TopAbstracts 然后可以像任何其他关联一样被包含。

关联级别的自定义解决方案

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


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

$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 &lt; 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 个选项都将查询和注入记录而无需任何黑客攻击,这不是很“直截了当”。


手动方法

为了完整起见,当然总是可以手动加载关联的记录并适当地格式化结果,例如使用结果格式化程序,例如参见 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

【讨论】:

  • 非常感谢!我最终使用了第三个选项,并且效果很好。对我来说足够“直截了当”:) 再次感谢
  • 我在stackoverflow.com/questions/32922440/… 也有类似的情况。区别在于文章和摘要之间不是一对多的。我的是多对多。我如何修改您的 3 个选项中的任何一个来实现这一点?
  • @ndm 。在我遇到此类问题的前几天。你在这里提到过现在它对我真的很有帮助。我了解了一个新事物,这是我在每个项目中都面临的最困难的事情。最后我以 .谢谢你,兄弟。我想给你投票。
  • 我已经尝试过“选择策略 - 在分组、最大值子查询上使用连接”的方式,但是当您有一个具有复合主键的表时,这显然不起作用。
  • @tiagoa 复合键通常应该可以正常工作,您可能想要打开一个新问题,您可以在其中正确详细说明您看到的问题,并提供重现它的代码示例。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-12
  • 1970-01-01
  • 2023-03-24
相关资源
最近更新 更多