真正要一次查完所有层级,得用嵌套集合或路径字段模型;用DB::select()执行WITH RECURSIVE CTE查询(MySQL 8.0+);创建时确保$fillable包含'parent_id'并显式转换类型。

用 with('children') 加载无限级分类时查出 N+1 问题
直接写 Category::with('children')->get() 看似能递归加载,但 Laravel 默认只预加载一级子类——第二级开始又会触发新查询,数据量稍大就卡死。
真正要一次查完所有层级,得用「嵌套集合(Nested Set)」或「路径字段(Path)」模型,而不是依赖 Eloquent 的关系链式加载。
- 别在
children关系里加with('children')做无限递归,这只会让查询数指数增长 - 改用单次查询 + PHP 层递归组装:先查出全部节点,按
parent_id归组,再用循环/引用构建树结构 - 如果数据稳定、读多写少,优先加
path字段(如"001.005.012"),查某节点所有后代只需where('path', 'like', '001.005.%')
whereHas() 查找带子孙的分类时结果为空
比如想找出“有至少一个三级子类”的一级分类,写 Category::whereHas('children.children') 看似合理,实际会失败——Eloquent 的 whereHas 不支持跨多层关系的条件穿透,底层生成的 SQL 会丢掉中间层级的 JOIN 条件。
本质是关系定义没覆盖“间接后代”,children 是一对多,但 children.children 在查询构造器里不被视为可下推的关联路径。
- 改用原生子查询:
whereExists()套两层SELECT,明确写出parent_id链路 - 或者给模型加个作用域,比如
scopeHasDescendant($query, $depth = 2),内部手动拼JOIN - 避免在
whereHas里写超过一层点号,像children.children.children几乎肯定查不到东西
用 DB::raw() 写递归 CTE 查询 MySQL 8.0+
MySQL 8.0+ 支持 WITH RECURSIVE,这是真正意义上的无限级查询方案,绕过 ORM 局限,性能也更好。但 Laravel 的 Query Builder 默认不支持递归 CTE,必须手写原始 SQL。
注意:不能把 CTE 当普通子查询塞进 DB::table()->where(...),它必须是语句最外层。
- 用
DB::select()执行完整 CTE 语句,例如:WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 1 as level FROM categories WHERE id = ? UNION ALL SELECT c.id, c.name, c.parent_id, t.level + 1 FROM categories c INNER JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree ORDER BY level
- 参数绑定要用
?占位符,别拼字符串,防止注入 - SQLite 和旧版 MySQL 不支持,上线前确认数据库版本,别在本地开发环境跑通就以为没问题
前端传 parent_id 创建子类时被忽略
用户提交表单带 parent_id=123,但新建记录的 parent_id 总是 null,大概率是模型的 $fillable 没放开这个字段,或者用了 create() 但没显式传入。
更隐蔽的情况是:你写了 Category::create($request->all()),但 $request->all() 里 parent_id 是字符串 "123",而数据库字段是整型,MySQL 严格模式下会转成 0,再被模型的 casts 或验证规则过滤掉。
- 检查
Category模型的$fillable数组是否包含'parent_id' - 创建时显式取值:
Category::create(['name' => $request->name, 'parent_id' => (int) $request->parent_id]) - 如果用表单请求类(Form Request),确保
rules()里写了'parent_id' => 'nullable|integer',否则验证直接失败
递归分类真正的复杂点不在怎么查,而在写入时怎么维护一致性——比如移动一个节点到另一个父级,path 字段、lft/rgt 值、甚至缓存都得同步更新。这些动作一旦漏掉一步,后续所有递归查询就全错。










