You need to enable JavaScript to run this app. Laravel基于Collection递归获取同表自关联分类所有子分类ID
优惠活动
大模型
产品
解决方案
定价
更多

Laravel基于Collection递归获取同表自关联分类所有子分类ID

分类子ID获取实现方案

核心目标:将数据库查询次数压缩到1次以内,彻底规避递归查询带来的N+1性能问题,完全匹配预期的Category::find(2)->allChildIds()调用形式。

实现思路

常规逐层级递归查询的写法,会随分类层级深度线性增加数据库查询次数(比如3层分类就会触发3次查询)。我们可以通过两种单查询的方案实现需求,根据分类表的实际数据量选择即可:

  • 内存递归映射方案:查询次数最少(全请求仅1次查询),小数据量下性能最优,可复用映射结果
  • 递归CTE方案:无全表内存开销,适合大表场景,依然保持单查询效率

方案1:内存递归映射(适合分类总量1万条以内的场景)

这种方案在单次请求生命周期内,无论调用多少次allChildIds()方法,整个流程只会触发1次数据库查询,是查询次数最少的实现。
直接在Category模型中新增如下代码即可,兼容你已经定义好的parent/childs关联:

// 全局缓存分类父子映射,避免同一次请求重复查库
protected static $categoryMap = null;

/**
 * 一次性加载全量分类的id与parent_id映射,仅查1次库
 */
protected static function loadCategoryMap()
{
    if (is_null(static::$categoryMap)) {
        // 仅查询必要字段,减少查询IO开销
        static::$categoryMap = static::query()
            ->select(['id', 'parent_id'])
            ->pluck('parent_id', 'id')
            ->toArray();
    }
}

/**
 * 获取当前分类下所有层级的子分类ID
 * @return array
 */
public function allChildIds(): array
{
    // 加载分类映射,全请求仅触发1次查询
    static::loadCategoryMap();

    $childIds = [];
    // 内存中递归收集所有子节点
    $this->collectChildren($this->id, static::$categoryMap, $childIds);

    return $childIds;
}

/**
 * 内存递归收集子节点,无额外数据库查询
 * @param int $parentId 当前查找的父分类ID
 * @param array $categoryMap 全量分类映射
 * @param array $result 收集结果的引用
 */
protected function collectChildren(int $parentId, array $categoryMap, array &$result)
{
    foreach ($categoryMap as $childId => $pid) {
        if ($pid === $parentId) {
            $result[] = $childId;
            // 递归查找当前子分类的下级节点
            $this->collectChildren($childId, $categoryMap, $result);
        }
    }
}

调用方式完全符合预期:

// 首次调用触发1次全量映射查询
$childIds = Category::find(2)->allChildIds();
// 同一次请求内再次调用其他分类的allChildIds,不会触发新的数据库查询
$otherChildIds = Category::find(5)->allChildIds();

方案2:递归CTE单SQL查询(适合分类总量超10万条的大表场景)

如果分类表数据量较大,全表拉取内存开销过高,可以使用数据库原生的递归CTE(MySQL8.0+、PostgreSQL等主流数据库均支持),依然保持1次数据库查询的效率,无需拉取全表数据:

/**
 * 获取当前分类下所有层级的子分类ID
 * @return array
 */
public function allChildIds(): array
{
    $sql = "WITH RECURSIVE category_tree AS (
                -- 先找到当前分类的直接子节点
                SELECT id FROM categories WHERE parent_id = ?
                UNION ALL
                -- 递归关联找到所有层级的子节点
                SELECT c.id FROM categories c
                INNER JOIN category_tree ct ON c.parent_id = ct.id
            )
            SELECT id FROM category_tree";
    
    return collect(\DB::select($sql, [$this->id]))->pluck('id')->toArray();
}

避坑提醒

不要使用逐层级触发关联查询的递归写法,这类写法会随分类深度增加数据库查询次数,产生严重的N+1问题:

// 反面示例:每次递归都会触发一次childs关联查询
public function badAllChildIds()
{
    $ids = $this->childs->pluck('id')->toArray();
    foreach ($this->childs as $child) {
        $ids = array_merge($ids, $child->badAllChildIds());
    }
    return $ids;
}

内容的提问来源于stack exchange,提问作者vidur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.31 20:27:05