Laravel怎么实现无限级分类_Laravel递归查询树形结构【进阶】

4次阅读

eloquent 的 with() 无法直接加载无限级分类,因其仅支持一级预加载,硬编码多层嵌套既受限又引发 n+1 问题;推荐闭包表模式配合事务维护祖先路径,再用 withdepth() 单次查询带层级扁平结果。

Laravel怎么实现无限级分类_Laravel递归查询树形结构【进阶】

为什么不能直接用 Eloquent 的 with() 加载无限级分类

因为 with() 默认只支持一级预加载,即使写成 with('children.children.children') 也得手动限定层级,且 N 层嵌套会触发 N 次查询(N+1 问题)或生成超长 sql。更关键的是:真实业务中树深不可控,比如后台可自由拖拽排序、移动节点,硬编码层级等于自埋雷。

withDepth() + defaultOrdering() 一步查出带层级的扁平结果

laravel 9+ 的 staudenmeir/laravel-cte 扩展(配合 postgresql/mysql 8.0+)能真正实现单次递归查询。但多数项目用的是 MySQL 5.7 或 sqlite,这时推荐「闭包表(Closure table)」模式 —— 即额外建一张 category_ancestors 表记录所有祖先路径:

CREATE TABLE category_ancestors (     ancestor_id BIGINT UNSIGNED NOT NULL,     descendant_id BIGINT UNSIGNED NOT NULL,     depth TINYINT UNSIGNED NOT NULL,     PRIMARY KEY (ancestor_id, descendant_id),     forEIGN KEY (ancestor_id) REFERENCES categories(id) ON DELETE CASCADE,     FOREIGN KEY (descendant_id) REFERENCES categories(id) ON DELETE CASCADE );

插入/移动节点时由模型事件自动维护该表。查某分类的所有子树只需:

Category::whereHas('ancestors', fn ($q) => $q->where('ancestor_id', $rootId))     ->withDepth()     ->orderBy('depth')     ->get();
  • 返回结果是扁平集合,每个模型带 $category->depth 属性
  • 避免了 PHP 层递归拼装,数据库原生排序更稳
  • 注意:withDepth() 是扩展包提供的方法,不是 Laravel 内置

前端渲染时怎么安全判断缩进和展开状态

别在 Blade 里写 @for($i = 0; $i depth; $i++)    @endfor —— 这种靠空格缩进既难调试又不利于无障碍访问。正确做法是:

  • 后端统一返回带 parent_iddepth 的数组,不拼 HTML
  • 前端用 CSS padding-left: calc(1rem * var(--depth)); 控制缩进
  • 折叠/展开状态由前端组件(如 Vue 的 v-if)控制,后端只提供 has_children 字段
  • 如果必须服务端渲染,用 <details><summary></summary></details> 原生标签,比 js 切换 class 更轻量

移动节点时最容易漏掉的三个事务点

把分类 A 移动到分类 B 下,看似只是改 parent_id,实际要同步处理:

  • 原父节点的子计数(children_count)要减 1
  • 新父节点的子计数要加 1
  • 闭包表中 A 的所有祖先路径要清空,并重新插入以 B 为起点的新路径(包括 B 自身)

这三步必须包裹在 DB 事务里,否则出现计数错乱或子树丢失。别信“先删再插”的简单逻辑 —— 如果中间出错,闭包表就残缺了,后续 withDepth() 查询直接失效。

text=ZqhQzanResources