Laravel GroupBy 数据分组及集合格式处理详解
2025-03-12 17:04:54
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
)的行为是解决问题的关键.