Laravel中无需foreach获取多分类的所有子分类及优化方案
优化多分类子分类查询方案
问题分析
原代码通过递归模型关联查询子分类,处理多个父分类时会产生N+1查询问题,性能低下。我们可以利用数据库**递归CTE(公共表表达式)**实现单查询获取所有层级子分类,同时支持返回ID数组或完整模型集合。
重构后的Category模型代码
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\DB; class Category extends Model { // 保留原有关联关系 public function subcategories() { return $this->hasMany(Category::class, 'parent_id'); } public function children() { return $this->subcategories()->with('children'); } /** * 批量获取多个父分类的所有子分类 * @param Category|Collection|array $categories 父分类实例、集合或ID数组 * @param bool $returnIds 是否返回ID数组(默认返回模型集合) * @return array|Collection */ public static function getAllDescendants($categories, $returnIds = false) { // 统一提取父分类ID列表 if ($categories instanceof Category) { $parentIds = [$categories->id]; } elseif ($categories instanceof Collection) { $parentIds = $categories->pluck('id')->toArray(); } elseif (is_array($categories)) { $parentIds = $categories; } else { return $returnIds ? [] : collect(); } // 递归CTE查询所有层级子分类 $descendants = DB::table('categories') ->withRecursive(['descendants' => function ($query) use ($parentIds) { // 初始节点:父分类本身 $query->select('*') ->from('categories') ->whereIn('id', $parentIds) // 递归查询子节点 ->unionAll(function ($query) { $query->select('c.*') ->from('categories as c') ->join('descendants as d', 'c.parent_id', '=', 'd.id'); }); }]) ->select('*') ->from('descendants') // 排除父分类本身(如果需要包含,删除此行) ->whereNotIn('id', $parentIds) ->get(); // 转换返回格式 if (!$returnIds) { // 将查询结果转换为Category模型集合 $descendants = self::hydrate($descendants->toArray()); } else { // 提取ID并转为一维数组 $descendants = $descendants->pluck('id')->toArray(); } return $descendants; } /** * 单个分类实例获取子分类(快捷方法) * @param bool $returnIds 是否返回ID数组 * @return array|Collection */ public function getDescendants($returnIds = false) { return self::getAllDescendants($this, $returnIds); } }
使用示例
1. 单个分类获取子分类ID
$category = Category::find($id); $childrenIds = $category->getDescendants(true);
2. 分类集合获取子分类ID
$categories = Category::where('parent_id', $id)->get(); $childrenIds = Category::getAllDescendants($categories, true);
3. 分类集合获取完整子分类集合
$categories = Category::where('parent_id', $id)->get(); $childrenCollection = Category::getAllDescendants($categories);
优化优势
- 单查询性能:通过CTE递归查询,无论多少父分类都只执行一次数据库查询,彻底解决N+1问题。
- 灵活兼容:支持传入单个实例、集合、ID数组三种输入类型。
- 双返回格式:通过参数控制返回ID数组(用于
whereIn查询)或完整模型集合。 - 无数据污染:移除原代码中持久化的类属性
$descendants,避免多次调用时的数据累积问题。
注意事项
- 确保数据库支持CTE:MySQL 8.0+、PostgreSQL、SQL Server等主流数据库均支持,若使用MySQL 5.x版本可改用嵌套集方案。
- 若需要包含父分类本身,删除CTE查询中的
->whereNotIn('id', $parentIds)语句。
内容的提问来源于stack exchange,提问作者0x9342812j1o
相关产品推荐
相关产品推荐

