Laravel Eloquent 如何统计无限层级分类下的关联文章总数?
无限层级分类文章总数统计实现方案
原实现的核心局限
- 原
countPosts方法仅遍历当前层级的直接子分类,未递归合并深层子分类的文章ID,最多只能统计到“当前分类+直接子分类”两级的文章数,层级更深的分类数据全部遗漏 - 递归过程中,子节点的统计结果没有向上传递,每次统计都重新从直接层级取数据,重复计算且覆盖不完整
- 递归查找子节点时反复遍历全量分类集合,数据量较大时性能损耗明显
- 模型中
withChildren方法硬编码嵌套7层关联加载,不属于真正的无限层级实现,还会和现有全量加载逻辑触发重复查询
具体改造方案
1. 预构建分类映射,优化递归查找效率
在内存中提前按parent_id对所有分类做分组映射,查找某分类的直接子节点时直接从映射数组取,将递归的时间复杂度从O(n²)降至O(n)。
在当前类中新增映射属性:
/** @var array<int, \Illuminate\Support\Collection> 按parent_id分组的分类映射 */ protected array $categoryMap;
2. 递归过程中同步汇总统计数据,沿树结构向上传递
不再依赖单独的countPosts方法做两级统计,而是在每个节点生成时,就把自身关联的文章ID、以及所有子节点上传的汇总文章ID合并去重,存到节点属性中,供上层父节点直接合并使用。该逻辑完全不依赖预设层级数,不管分类树有多少层,都能完整覆盖所有节点的统计。
修改buildMenu初始化逻辑:
private function buildMenu(): void { $this->menu = new Collection(); $allCategories = PostCategory::with('posts')->orderBy('order')->get(); // 预构建分类映射:key为parent_id,value为对应父级下的直接子分类集合 $this->categoryMap = $allCategories->groupBy('parent_id')->toArray(); $topLevelCategories = $this->categoryMap[null] ?? []; foreach ($topLevelCategories as $postCategory) { $subChildren = $this->getChildren($postCategory, 1); // 顶级分类统计:自身文章 + 所有子节点汇总的文章ID $currentPostIds = $postCategory->posts->pluck('id')->toArray(); foreach ($subChildren as $child) { $currentPostIds = array_merge($currentPostIds, $child->allPostIds); } $totalCount = count(array_unique($currentPostIds)); $this->menu->push(new MenuItem([ 'count' => $totalCount, 'children' => $subChildren, 'offset' => 0, 'category' => null, ])); } $this->menu->prepend(new MenuItem([ 'count' => $this->getAllPostCount(), 'children' => collect([]), 'offset' => 0, 'category' => null, ])); }
修改递归生成子节点的getChildren方法:
private function getChildren(PostCategory $parentCategory, int $offset): Collection { $children = collect([]); // 直接从预构建映射取当前分类的直接子分类 $directChildren = $this->categoryMap[$parentCategory->id] ?? []; foreach ($directChildren as $postCategory) { // 递归生成所有深层子节点 $subChildren = $this->getChildren($postCategory, $offset + 1); // 汇总当前节点的所有关联文章ID:自身文章 + 所有子节点上传的汇总ID $currentPostIds = $postCategory->posts->pluck('id')->toArray(); foreach ($subChildren as $child) { $currentPostIds = array_merge($currentPostIds, $child->allPostIds); } $uniquePostIds = array_unique($currentPostIds); $children->push(new MenuItem([ 'count' => count($uniquePostIds), 'children' => $subChildren, 'offset' => $offset, 'category' => $postCategory, // 存储当前节点汇总的全量文章ID,供上层父节点合并 'allPostIds' => $uniquePostIds, ])); } return $children; }
3. 冗余逻辑清理
- 删除原
countPosts方法,避免和新的递归统计逻辑重复 - 移除模型中硬编码嵌套层级的
withChildren方法,当前逻辑已经在内存中全量加载所有分类,不需要额外通过关联递归查询,可减少不必要的数据库查询开销
方案特性
- 统计逻辑沿着树结构从最深层叶子节点开始,每往上一层就合并当前层节点的文章ID和下层传上来的汇总ID,全程只需要一次递归遍历,不需要重复深入深层节点
- 全程做文章ID去重,就算同一篇文章被绑定到多个子分类下,统计时也不会重复计数
- 适配任意层级深度的分类树,不需要提前预设层级数
- 若存在分类量、文章量极大的场景,可将存储在MenuItem中的
allPostIds调整为全局静态映射(按节点ID索引对应ID集合),避免重复存储造成的内存浪费
内容的提问来源于stack exchange,提问作者Miner
相关产品推荐
相关产品推荐

