laravel父子层级查询核心是with闭包递归加载+边界控制,而非硬编码with链;数据库cte或path字段方案用于后代判断,避免php遍历;获取子集须用pluck()->toarray()配合wherein,且需校验id数组非空。

直接说结论:Laravel 中父子层级查询不是靠“一层层写 with”硬编码解决的,核心是用 with 闭包递归加载 + 合理控制查询边界,否则容易 N+1 或爆内存。
用 with 闭包实现真正递归加载
硬写 with('children.children.children') 看似简单,但层级一变就得改代码,且无法动态控制深度。Laravel 9+ 支持闭包式嵌套预加载,这才是可维护的做法:
- 在模型中定义一个作用域(scope):
public function scopeWithChildren($query) { return $query->with(['children' => fn ($q) => $q->withChildren()]); } - 调用时只需:
$tree = Category::whereNull('parent_id')->withChildren()->get(); - 该写法会自动展开到叶子节点,但注意:它仍是一次性把整棵树拉进内存,适合 ≤ 5 层、几百条数据的场景
查某节点是否属于指定祖先的后代
别用 PHP 遍历整棵树判断——数据量稍大就卡死或超时。数据库原生递归 CTE 才是正解(MySQL 8.0+/PostgreSQL):
- 在模型里加一个作用域:
public function scopeDescendantsOf(Builder $builder, int $ancestorId): Builder { return $builder->fromSub(function ($query) use ($ancestorId) { $query->selectRaw('id') ->fromRaw('(WITH RECURSIVE tree AS ( SELECT id, parent_id FROM categories WHERE id = ? UNION ALL SELECT c.id, c.parent_id FROM categories c INNER JOIN tree t ON c.parent_id = t.id ) SELECT id FROM tree) as descendants', [$ancestorId]); }, 'descendants') ->join('descendants', 'categories.id', '=', 'descendants.id'); } - 使用方式极简:
$isDescendant = Category::descendantsOf(1)->where('id', 24)->exists(); - 不支持 CTE 的旧版 MySQL?只能退回到“路径字段”方案(如
path字段存"1/5/24"),但需业务层维护一致性
获取最新父记录及其全部子记录时的致命错误
常见错误是把模型对象当 ID 传给子查询,导致 where('parent_id', $parent) 永远不匹配:
-
Child::where('parent_id', $parent)❌ ——$parent是 Eloquent 对象,PHP 对象与整数比较恒为 false -
Child::where('parent_id', $parent->id)✅ —— 必须显式取主键值 - 父记录要用
firstOrFail()而非limit(1)->get(),前者返回模型实例,后者返回集合,取id多一层[0]->id容易漏判空 - 子记录也建议加
orderBy('id', 'desc'),否则顺序不可控
用 whereIn 批量查子集时别忘了 pluck()->toArray()
拿到一堆父级模型后,想查它们的所有子级,不能直接 $parents->id —— 集合没有 id 属性:
-
$parentIds = $parents->pluck('id')->toArray();✅ 提取 ID 数组 -
$children = Category::whereIn('parent_id', $parentIds)->get();✅ 传数组进whereIn - 如果父集为空(比如没根节点),
whereIn会变成WHERE parent_id IN (),MySQL 报错;加个if (!empty($parentIds)) { ... }更稳妥
递归查询真正的难点不在语法,而在边界控制:什么时候该用数据库 CTE,什么时候该加 path 字段,什么时候必须分页懒加载——这些没法靠一个 scope 解决,得看数据规模和层级深度。











