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
相关产品推荐
相关产品推荐

