返回

Laravel GroupBy 数据分组及集合格式处理详解

php

Laravel 中使用 Group By 获取数据并处理集合格式问题

开发过程中,经常会遇到需要对数据库查询结果进行分组(Group By)处理的情况。有时,数据计算逻辑比较复杂,需要在获取数据后再做处理, 然后再分组,此时就可能碰到一些格式问题, 就像上面那个例子一样。 这篇文章就来聊聊在 Laravel 中如何解决类似问题。

问题重现

直接看这段代码:

$linedata = GetMyData::select('id', 'runtime_batch', 'smallstoptime_batch', 'downtime_batch', 'output', 'output_final')->get();

foreach($linedata as $ld) {

  $ld->runtime_batch = round(($ld->runtime_batch+$ld->smallstoptime_batch)/($ld->runtime_batch+$ld->smallstoptime_batch+$ld->downtime_batch)*100, 2);
  $ld->downtime_batch = round($ld->output/$ld->output_final*100, 2);
  $ld->smallstoptime_batch = round($ld->output_final/($ld->runtime_batch+$ld->smallstoptime_batch)*100, 2);
}

$groupingline = collect($linedata)->groupBy('id')->values()->all();

获得的数据输出是这样的:

"data": [
        [
            {
                "id": 2,
                "runtime_batch": 24528,
                "smallstoptime_batch": 250,
                "downtime_batch": 200,
            },
            {
                "id": 2,
                "runtime_batch": 2074,
                "smallstoptime_batch": 0,
                "downtime_batch": 1,
            }
        ],
        [
            {
                "id": 3,
                "runtime_batch": 185,
                "smallstoptime_batch": 252,
                "downtime_batch": 1409,
            },
            {
                "id": 3,
                "runtime_batch": 2127,
                "smallstoptime_batch": 377,
                "downtime_batch": 22452,
            }
        ]
    ]

想要的结果是去除每个分组外层的 [],直接得到一个对象数组,例如,针对ID=2的分组应该是:

{
    "id": 2,
    "runtime_batch": 24528,
    "smallstoptime_batch": 250,
    "downtime_batch": 200,
},
{
    "id": 2,
    "runtime_batch": 2074,
    "smallstoptime_batch": 0,
    "downtime_batch": 1,
}

而不是被包裹在[]中。

问题原因分析

问题的根源在于groupBy('id')->values()->all() 这几步操作。 groupBy('id') 没问题,它会根据 id 字段将集合中的元素分组。但后面的 values()all() 就有点儿意思了。

  • values(): 这一步会将分组后的集合的键(keys)丢弃,只保留值(values)。每个值就是一个包含相同id的子集合。这导致每个分组都变成了一个新的集合。
  • all(): 将 Collection 转化成 原生 PHP 数组, 最终的结果就是一个二维数组。

这就是为什么输出结果中每个分组都被方括号[]包围的原因。

解决方案

既然搞清楚了问题所在,解决起来就简单了。下面给出几种解决方案:

1. 直接去掉 values()all()

最简单的办法就是直接把values()all()去掉。这样,groupBy 之后,保留每个分组的key(也就是id),每个分组还是一个Collection

$linedata = GetMyData::select('id', 'runtime_batch', 'smallstoptime_batch', 'downtime_batch', 'output', 'output_final')->get();

foreach($linedata as $ld) {
    $ld->runtime_batch = round(($ld->runtime_batch+$ld->smallstoptime_batch)/($ld->runtime_batch+$ld->smallstoptime_batch+$ld->downtime_batch)*100, 2);
    $ld->downtime_batch = round($ld->output/$ld->output_final*100, 2);
    $ld->smallstoptime_batch = round($ld->output_final/($ld->runtime_batch+$ld->smallstoptime_batch)*100, 2);
}

$groupingline = collect($linedata)->groupBy('id');

//如果需要转成数组,在后续处理,循环 $groupingline
// foreach($groupingline as $id => $group) {
//    $groupArray = $group->all();  //此时$groupArray 是一个不带外层[]的数组.
//    //$groupArray 进行其他操作...
//}

这样得到的 $groupingline 是一个以 id 为键, 每个键对应的值是一个 Collection 对象的集合。 如果要将每个分组转换成普通数组,可以在后续处理中根据需要使用all()方法。

原理: groupBy方法默认保留键。这样做的好处是你可以继续利用Collection 提供的丰富的方法对每个分组进行后续处理,灵活性更高。

2. 使用 map 方法

如果你确实需要一个二维数组, 并且想去掉外层括号, 可以在groupBy后使用map方法。

$linedata = GetMyData::select('id', 'runtime_batch', 'smallstoptime_batch', 'downtime_batch', 'output', 'output_final')->get();

foreach($linedata as $ld) {
  $ld->runtime_batch = round(($ld->runtime_batch+$ld->smallstoptime_batch)/($ld->runtime_batch+$ld->smallstoptime_batch+$ld->downtime_batch)*100, 2);
  $ld->downtime_batch = round($ld->output/$ld->output_final*100, 2);
  $ld->smallstoptime_batch = round($ld->output_final/($ld->runtime_batch+$ld->smallstoptime_batch)*100, 2);
}

$groupingline = collect($linedata)->groupBy('id')->map(function ($item) {
    return $item->values()->all();
});

原理:
map方法会遍历集合中的每一个元素(在这里,每一个元素就是一个分组),并对每个元素执行回调函数。 回调函数function ($item) { return $item->values()->all(); } 接收每个分组 ($item) 作为参数,并对该分组调用values()->all()。这样就得到了不带外层方括号的数组, 并保留了最外层的数组结构。

3. 在数据库查询阶段进行分组和计算 (推荐)

上面的两种方法都是在获取全部数据后,在 PHP 中进行的计算和分组。更推荐的做法是, 尽量在数据库查询阶段就完成分组和计算,这样效率更高,代码也更简洁。

$groupingline = GetMyData::select(
    'id',
    DB::raw('ROUND(SUM(runtime_batch + smallstoptime_batch) / SUM(runtime_batch + smallstoptime_batch + downtime_batch) * 100, 2) as runtime_batch'),
    DB::raw('ROUND(SUM(output) / SUM(output_final) * 100, 2) as downtime_batch'),
    DB::raw('ROUND(SUM(output_final) / SUM(runtime_batch + smallstoptime_batch) * 100, 2) as smallstoptime_batch')
)
->groupBy('id')
->get();

原理:
直接在SQL查询中使用聚合函数(SUM)和DB::raw来执行计算, 然后用groupBy('id')分组。 这样从数据库取出的数据直接就是分组和计算好的结果,避免了在 PHP 中循环和处理大量数据。

进阶技巧 : 假设你需要计算每个id下, runtime_batch大于某个值的记录数量,可以这样做:


$groupingline = GetMyData::select(
    'id',
        DB::raw('ROUND(SUM(runtime_batch + smallstoptime_batch) / SUM(runtime_batch + smallstoptime_batch + downtime_batch) * 100, 2) as runtime_batch'),
        DB::raw('ROUND(SUM(output) / SUM(output_final) * 100, 2) as downtime_batch'),
        DB::raw('ROUND(SUM(output_final) / SUM(runtime_batch + smallstoptime_batch) * 100, 2) as smallstoptime_batch'),
    DB::raw('COUNT(CASE WHEN runtime_batch > 1000 THEN 1 END) as high_runtime_count') //新增的count
)
->groupBy('id')
->get();

安全建议: 当使用DB::raw时, 要小心SQL注入风险. 确保你拼接的SQL语句是安全的, 不要直接拼接用户输入的内容。如果需要拼接用户输入, 使用参数绑定。例如:

$minRuntime = 1000; //假设来自用户输入
$groupingline = GetMyData::select(
   'id',
    DB::raw('COUNT(CASE WHEN runtime_batch > ? THEN 1 END) as high_runtime_count', [$minRuntime]) //使用参数绑定
)
->groupBy('id')
->get();

4.使用toArray()和toJson()控制最终结果格式。

根据输出需要(例如 API 接口),可以使用 toArray()或 toJson()方法

$groupingline = collect($linedata)->groupBy('id');

$arrayResult = $groupingline->toArray(); // 转成普通数组

$jsonResult = $groupingline->toJson();  // 转成 JSON 字符串

//输出结果类似于:
//{
//    "2":[
//         {"id":2,"runtime_batch":99.21,"smallstoptime_batch":95.91,"downtime_batch":100},
//         {"id":2,"runtime_batch":99.95,"smallstoptime_batch":0,"downtime_batch":0}
//         ],
//    "3":[
//            {"id":3,"runtime_batch":43.15,"smallstoptime_batch":24.86,"downtime_batch":7.95},
//           {"id":3,"runtime_batch":8.8,"smallstoptime_batch":7.79,"downtime_batch":8.6}
//        ]
//}

这种方法比较适合输出标准的数组, 方便前端处理. 得到的就是经典的分组数据结构, 外层 key 是 分组的id, 内部是 各个数据对象.

总结

选择哪种解决方案,要看你的具体需求。 如果数据量不大,在 PHP 中进行计算和分组也OK.但通常情况下,推荐尽量在数据库查询阶段就完成分组和计算,性能更好,代码更清晰。 不管哪种方式,理解 groupBy以及相关方法(values, all, map)的行为是解决问题的关键.