You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 23:06:28