如何在Eloquent关联中对数据透视表列进行分组和求和?

14
在 Laravel 4 中,我有一个名为 ProjectPart 的模型,它们之间存在多对多的关系,并使用一个名为 project_part 的中间表来实现。中间表包含一个名为 count 的列,用于表示每个部件 ID 在项目中使用的次数,例如:
id  project_id  part_id count
24  6           230     3

这里的project_id为6,使用了3个part_id为230的零件。

同一项目中可能会多次列出同一零件,例如:

id  project_id  part_id count
24  6           230     3
92  6           230     1

当我展示我的项目的部件清单时,我不希望重复显示part_id,因此我将结果分组。

我的Projects模型如下:

public function parts()
{
    return $this->belongsToMany('Part', 'project_part', 'project_id', 'part_id')
         ->withPivot('count')
         ->withTimestamps()
         ->groupBy('pivot_part_id')
}

但是我的count值当然不正确,这里就出现了我的问题:如何获得项目所有分组部分的总和?

也就是说,我的project_id为6的零件清单应该是这样的:

part_id count
230     4

我希望它能在项目-零件关系中,这样我就可以急切加载它。

我无法理解如何在不出现N+1问题的情况下做到这一点,任何见解都将不胜感激。


更新: 作为一个临时的解决办法,我创建了一个Presenter方法来获取项目中零件的总数。但这给我带来了N+1问题。

public function sumPart($project_id)
{
    $parts = DB::table('project_part')
        ->where('project_id', $project_id)
        ->where('part_id', $this->id)
        ->sum('count');

    return $parts;
}
3个回答

32
尝试在集合(Collection)中求和。
$project->parts->sum('pivot.count');

这是我找到的最佳方式。它很干净(易于阅读),并能够重复使用您所有的范围、排序和关系属性缓存在parts的多对多定义中。

@hebron 如果您使用with('parts')预加载,则此解决方案不会出现N+1问题。因为$project->parts没有函数调用)是一个缓存的属性,返回一个包含所有数据的Collection实例。而sum('pivot.count')Collection的一个方法,其中包含纯函数式助手(与数据库无关,就像js世界中的underscore一样)。

完整示例:

关系部件的定义:

class Project extends Model
{
    public function parts()
    {
        return $this->belongsToMany('Part', 'project_part', 'project_id', 'part_id')
            ->withPivot('count')
            ->withTimestamps();
    }
}

当您使用它时(请注意,渴望地加载很重要,以避免N+1问题),

App\Project::with('parts')->get()->each(function ($project) {
    dump($project->parts->sum('pivot.count'));
});

或者你可以在Project.php中定义sum函数,

class Project extends Model
{
    ...

    /**
     * Get parts count.
     *
     * @return integer
     */
    public function partsCount()
    {
        return $this->parts->sum('pivot.count');
    }
}

如果你想避免在调用端使用 with('parts') (默认情况下急加载部件),你可以添加一个 $with 属性

class Project extends Model
{
    /**
     * The relations to eager load on every query.
     *
     * @var array
     */
    protected $with = ['parts'];

    ...
}

1
这样做会不会导致 N+1 问题? - Thomas Jensen
@hebron 这个解决方案没有N+1问题。示例已更新。 - chuangbo

10

根据代码源

我们需要使用“pivot_”前缀别名所有的中间表列,以便在检索和填充模型时可以轻松地将它们提取出来并放入中间关系中。

因此您可以使用select方法实现同样的功能。

public function parts()
{
    return $this->belongsToMany('Part', 'project_part', 'project_id', 'part_id')
        ->selectRaw('parts.*, sum(project_part.count) as pivot_count')
        ->withTimestamps()
        ->groupBy('project_part.pivot_part_id')
}

看起来这个可行,只是我还得在 selectRaw 中加上 parts.*,否则我就拿不到其他字段了。我会彻底测试一下并告诉你结果。谢谢 :) - Thomas Jensen
我认为你不需要添加 parts.*,Eloquent会在幕后自动添加它。参考链接 - Razor
这也是我想的,但如果不添加它,它就不是查询的一部分。 - Thomas Jensen
非常奇怪,你是如何构建查询的?你能分享已执行的查询吗?你使用的是哪个版本?如果没有"part.*",具体会得到什么(请进行转储)? - Razor
我指的是确切的版本4.2.(?)...无论如何,我用一个旧版本4.2.1进行了测试,它完美地工作了,但对于L4.2.16来说,似乎你必须添加parts.*这更合理(答案已更新)。需要注意的是,当你使用连接操作(这是belongsToMany的情况)时,最好在列名中添加表名orderBy('tableName.description_en') - Razor
显示剩余3条评论

2
最佳的使用方式是:
$project->parts->sum('pivot.count');

我遇到了同样的问题,但是这个解决了我的问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接