如何通过 Eloquent 中具有多对多关系的外部表中的列聚合进行分组?

Posted

技术标签:

【中文标题】如何通过 Eloquent 中具有多对多关系的外部表中的列聚合进行分组?【英文标题】:How to group by aggregation of column in foreign table with many-to-many relation in Eloquent? 【发布时间】:2019-06-25 14:29:34 【问题描述】:

我开始为此头疼,所以我想我只是在这里发布。 我有两个通过数据透视表关联的表(因为它是多对多关系)。我使用 Laravel 和 Eloquent(但也非常感谢有关如何使用普通 SQL 查询实现此目的的一般帮助)。 我想根据第二个表的列订购第一个表,但为此需要“聚合”列。

以由许多司机共享并且可以有不同颜色的汽车为例:

Car-Table: [id, color]  
Driver-Table: [id, name]  
Car.Driver-Table: [car_id, driver_id]

我需要一个查询来获取所有驾驶红色汽车的司机,然后是所有驾驶红色汽车的司机。 我必须使用查询,因为之后我可能会对此查询执行其他操作(如过滤),并希望最终分页。 我已经使用了获取两个组之一的查询。它们看起来像这样: 在 Driver 模型中:

public function redCars() 
    return $this->cars()->where('color', 'red');


public function otherColoredCars() 
    return $this->cars()->where('color', '<>', 'red');

然后在控制器的某个地方:

$driversWithOnlyRedCars = Driver::whereDoesntHave('otherColoredCars')->get();
$driversWithoutRedCars = Driver::whereDoesntHave('redCars')->get();

有没有办法将这两者结合起来? 也许我在这里的想法完全错误。

更新澄清: 基本上我需要这样的东西(或任何其他会导致相同结果的方式)

$driversWithOnlyRedCars->addTemporaryColumn('order_column', 0); // Create temporary column with value 0
$driversWithoutRedCars->addTemporaryColumn('order_column', 1);
$combinedQuery = $driversWithOnlyRedCars->combineWith($driversWithoutRedCars); // Somehow combine both queries
$orderedQuery = $combinedQuery->orderBy('order_colum');
$results = $combinedQuery->get();

更新 2 我想,我发现了如何通过原始查询接近我的目标。 会是这样的:

$a = DB::table(DB::raw("(
  SELECT id, 0 as ordering
  FROM drivers
  WHERE EXISTS (
    SELECT * FROM cars
    LEFT JOIN driver_car ON car.id = driver_car.car_id
    WHERE driver.id = driver_car.driver_id
    AND cars.color = 'red'
  )
) as only_red_cars"));

$b = DB::table(DB::raw("(
  SELECT id, 1 as ordering
  FROM drivers
  WHERE EXISTS (
    SELECT * FROM cars
    LEFT JOIN driver_car ON car.id = driver_car.car_id
    WHERE driver.id = driver_car.driver_id
    AND cars.color <> 'red'
  )
) as no_red_cars"));

$orderedQuery = $a->union($b)->orderBy('ordering');

现在的问题是我需要像这样订购并最终分页的模型,所以这并不是我问题的真正答案。我试图将其转换回模型,但我还没有成功。我尝试了什么:

$queriedIds = array_column($orderedQuery->get()->toArray(), 'id');
$orderedModels = Driver::orderByRaw('(FIND_IN_SET(drivers.id, "' .  implode(',', $queriedIds) . '"))');

但看起来FIND_IN_SET 只允许将表的列作为第二个参数。是否有另一种方法可以从有序联合查询中以正确的顺序获取模型?

【问题讨论】:

情侣问题; whereDoesntHave() 需要 2 个参数、一个关系名称和其他查询逻辑。如果你不想传递额外的逻辑,那就是doesntHave()。其次,我认为它不适用于otherColoredCars,因为这不完全是一种关系。您可以使用 whereDoesntHave('cars', function($subQuery) ... ); 并将 ... 替换为仅省略/包含红色汽车等的逻辑。 嗯,这实际上不可行,除非我写下这个小例子时犯了一个错误。这就是我在我的项目中所做的,我从中得到了想要的结果。 您的意思是UNION 查询吗? @JonasStaudenmeir 我刚刚发现了如何为此使用联合,现在当我回来为我的问题添加更新时看到了你的评论:) 也许你可以看看。 【参考方案1】:

您可以使用UNION 查询:

$driversWithOnlyRedCars = Driver::select('*', DB::raw('0 as ordering'))
    ->whereDoesntHave('otherColoredCars');

$driversWithoutRedCars = Driver::select('*', DB::raw('1 as ordering'))
    ->whereDoesntHave('redCars');

$drivers = $driversWithOnlyRedCars->union($driversWithoutRedCars)
    ->orderBy('ordering')
    ->orderBy('') // TODO
    ->paginate();

您希望如何订购具有相同 ordering 的驱动程序?您应该在每次执行查询时添加第二个ORDER BY 子句以获得一致的顺序。

【讨论】:

这看起来和我一直在寻找但想不出的东西一模一样。我会尽快尝试,如果有效,我会将其标记为答案。谢谢【参考方案2】:

这是我得到的最好的:

$driversWithOnlyRedCars = Driver::whereHas('cars',function($q)
  $q->where('color', 'red');
)->withCount('cars')->get()->where('cars_count',1);

【讨论】:

我不确定,但我可能会尝试一下(也许再次^^)。但如果这行得通,为什么这能确保只有红色汽车的司机排在其他人之前呢?而且我想我以后不能再做所有其他可能的查询了? (比如当所有司机拥有相同的“汽车状态”时,按姓名对他们进行排序?) 哦,对不起,我读错了你如何命名结果。据我所知,这不会让只驾驶红色汽车的司机,而是所有驾驶至少一辆红色汽车(可能还有其他任何一辆)的司机。请参阅文档中 whereHas 的第一个示例:laravel.com/docs/5.8/…。此外,查询所有只有红色汽车的司机并不能解决我的问题,因为我想要一个列表,其中所有只有红色汽车的司机首先列出,所有其他司机列在他们之后。 感谢您的帮助,也许我可以以某种方式使用它,但这并不能解决我的问题。这只是我已经以另一种方式做的事情的另一种方式(让所有司机只驾驶红色汽车)。我将编辑我的问题,以更清楚地说明我需要什么。

以上是关于如何通过 Eloquent 中具有多对多关系的外部表中的列聚合进行分组?的主要内容,如果未能解决你的问题,请参考以下文章

Eloquent 中与分类学的多对多关系

Laravel Eloquent 多对多,增量依赖

Laravel Eloquent 根据数据透视表字段条件获取多对多关系

如何在 Eloquent ORM 中按多对多关系的数据透视表的字段进行排序

Laravel Eloquent: hasManyThrough 多对多关系

laravel 返回与 eloquent 和 laravel 的关系