三、实现订单菜品按分类分组
我们的目标是在查询一个订单时,能够将其包含的所有菜品按照其所属的分类进行分组展示。这通常需要在控制器或服务层中完成。
3.1 基础查询:获取订单及其菜品
首先,我们通过预加载dishes关联来获取订单数据,并同时加载每个菜品所属的分类信息,以避免 N+1 查询问题。
// 在控制器或服务方法中
$order = Order::with(['dishes.category'])->find($orderId);
// 此时,$order->dishes 是一个菜品集合
// 每个 $dish 对象都可以通过 $dish->category 访问其分类
foreach ($order->dishes as $dish) {
echo $dish->name . ' 属于分类:' . $dish->category->name;
}3.2 在集合层面进行分组
获取到数据后,我们可以利用 Laravel 集合强大的groupBy方法,在内存中对菜品进行分组。
$order = Order::with(['dishes.category'])->find($orderId);
// 对菜品集合按分类ID进行分组
$groupedDishes = $order->dishes->groupBy(function ($dish) {
// 这里返回作为分组键的值,我们使用分类ID
return $dish->category->id;
});
// 或者,直接按分类关系对象分组(需要确保分类已加载)
$groupedDishes = $order->dishes->groupBy('category.id');
// 遍历分组后的结果
foreach ($groupedDishes as $categoryId => $dishesInCategory) {
// $dishesInCategory 是属于同一分类的菜品集合
$firstDish = $dishesInCategory->first();
echo "分类: " . $firstDish->category->name . "n";
foreach ($dishesInCategory as $dish) {
echo " - " . $dish->name . " x " . $dish->pivot->quantity . "n";
}
}这种方法的优点是简单直接,利用了 Eloquent 和集合的特性。缺点是将所有数据加载到内存后进行分组,如果订单包含的菜品数量极大,可能会有性能影响。
3.3 在数据库查询层面进行分组与聚合
对于更复杂的场景或追求极致的查询效率,我们可以直接通过数据库查询构建器进行分组和聚合。这通常需要更复杂的连接(Join)查询。
例如,我们想直接获取订单中每个分类的菜品总数量和总金额:
use IlluminateSupportFacadesDB;
$stats = DB::table('orders')
->join('order_dish', 'orders.id', '=', 'order_dish.order_id')
->join('dishes', 'order_dish.dish_id', '=', 'dishes.id')
->join('dish_categories', 'dishes.category_id', '=', 'dish_categories.id')
->select(
'dish_categories.id as category_id',
'dish_categories.name as category_name',
DB::raw('SUM(order_dish.quantity) as total_quantity'),
DB::raw('SUM(order_dish.quantity * order_dish.unit_price) as total_amount')
)
->where('orders.id', $orderId)
->groupBy('dish_categories.id', 'dish_categories.name')
->get();
foreach ($stats as $stat) {
echo "分类: {$stat->category_name}, 总数量: {$stat->total_quantity}, 小计: {$stat->total_amount}n";
}这种方法将分组和计算逻辑完全下推到数据库,效率最高,但失去了 Eloquent 模型的便利性和面向对象的特性。查询结果是一个标准对象集合,而不是 Dish 模型实例。
四、优化与高级用法
4.1 在 Order 模型中定义分组查询作用域
为了代码复用,我们可以在Order模型中定义一个本地作用域,专门用于获取带分组的订单数据。
// app/Models/Order.php
public function scopeWithGroupedDishes($query)
{
return $query->with(['dishes' => function ($query) {
// 在加载dishes时,预先按category_id排序,方便后续处理
$query->orderBy('category_id');
}, 'dishes.category']);
}
// 使用方式
$order = Order::withGroupedDishes()->find($orderId);
// 然后可以在业务逻辑中使用集合的 groupBy
$grouped = $order->dishes->groupBy('category.name');4.2 使用访问器生成分组数据
我们可以为Order模型定义一个访问器,使其可以直接返回分组好的菜品数据,将分组逻辑封装在模型内部。
// app/Models/Order.php
protected $appends = ['grouped_dishes']; // 将访问器追加到模型数组/JSON
public function getGroupedDishesAttribute()
{
if (!$this->relationLoaded('dishes')) {
// 如果dishes关系尚未加载,则触发加载(可能产生额外查询)
$this->load(['dishes.category']);
}
return $this->dishes->groupBy(function ($dish) {
// 返回一个结构更清晰的分组数组
return [
'category_id' => $dish->category->id,
'category_name' => $dish->category->name,
];
})->map(function ($dishes, $key) {
// 对每个分组进行处理,$key是上面返回的数组
$keyArray = json_decode($key, true); // 因为groupBy的键是数组的JSON字符串
return [
'category' => [
'id' => $keyArray['category_id'],
'name' => $keyArray['category_name'],
],
'dishes' => $dishes->map(function ($dish) {
return [
'id' => $dish->id,
'name' => $dish->name,
'quantity' => $dish->pivot->quantity,
'unit_price' => $dish->pivot->unit_price,
'subtotal' => $dish->pivot->quantity * $dish->pivot->unit_price,
];
}),
'category_total' => $dishes->sum(function ($dish) {
return $dish->pivot->quantity * $dish->pivot->unit_price;
}),
];
})->values(); // 重置键为连续数字索引
}
// 使用方式:获取订单后,可以直接访问 $order->grouped_dishes
$order = Order::with(['dishes.category'])->find($orderId);
$groupedData = $order->grouped_dishes; // 返回一个已分组的、结构化的数组这种方式将复杂的业务逻辑封装在模型中,控制器可以保持简洁,并且数据格式对于前端API响应非常友好。
4.3 处理 N+1 查询问题
在分组场景中,如果不使用with()进行预加载,当遍历菜品并访问$dish->category时,会产生大量的数据库查询(N+1问题)。因此,务必确保在查询订单时,使用with(['dishes.category'])或load(['dishes.category'])一次性加载所有需要的关系数据。
五、总结
通过 Eloquent 关联模型实现订单菜品分组,体现了 Laravel 框架在数据关系处理上的灵活与强大。我们可以根据具体需求选择不同的实现策略:
简单场景:使用
with()预加载结合集合的groupBy()方法,快速在应用层完成分组,代码清晰易懂。复杂聚合与高性能需求:使用查询构建器编写原生 SQL 分组查询,将计算压力转移至数据库。
追求代码结构与复用性:在模型中定义查询作用域或访问器,将分组逻辑封装起来,提供简洁的 API 给控制器使用。
关键在于理解 Eloquent 关联(<belongsToMany>, <belongsTo>, <hasMany>)的工作原理,并熟练运用预加载(Eager Loading)来避免性能陷阱。结合 Laravel 集合提供的丰富方法,我们能够以优雅且高效的方式处理诸如订单菜品分组这类常见的业务数据组织需求。