直接用 with('children') 会爆栈或查出重复数据,是因为递归加载未设终止条件,eloquent 的 with() 不支持无限嵌套,且未做循环检测、id 缓存或软删除兼容处理。

为什么直接用 with('children') 会爆栈或查出重复数据
这不是 Eloquent 的 bug,而是递归加载没设终止条件的必然结果。Eloquent 的 with() 本身不支持无限嵌套,写成 with('children.children.children') 是硬编码层级,一旦真实树深超过预设值,漏掉的节点就查不到;若改成闭包递归调用自身(比如在 scope 里反复调 withChildren()),PHP 会不断新建查询上下文,最终内存耗尽或达到 xdebug.max_nesting_level 限制报错。
常见错误现象包括:Fatal error: Allowed memory size exhausted、Maximum function nesting level reached、返回结果里同一节点出现多次——根本原因是没缓存已处理过的 ID,或没过滤已加入树的节点。
- 别在模型的
accessor或scope里隐式触发递归查询,例如getTreeAttribute中又调$this->children - 不要依赖
with()自动推导关系深度,它不会帮你做循环检测 - 如果用了软删除,
children()关联默认不包含已软删节点,树会在断点处截断,得显式加withTrashed()
用 buildTree() 手动组装前必须检查三件事
这个函数看似简单,但 80% 的空数组、Array to string conversion 错误都源于底层数据没对齐。核心不是“怎么写递归”,而是“怎么让 PHP 能准确把子节点挂到父节点下”。
-
id和parent_id字段类型必须一致:都为unsignedBigInteger,且 PHP 中用(int)强转后再比较,避免字符串"1"匹配整数1失败 -
parent_id必须允许为null(根节点),不能设NOT NULL DEFAULT 0,否则插入根节点时违反约束 - 先用
groupBy('parent_id')把扁平集合分组,而不是每次filter()遍历全量数据——后者是 O(n²),几百条数据就明显卡顿
示例关键片段:
public function buildTree($groups, $parentId = null)
{
$items = $groups->get($parentId, collect());
return $items->map(function ($item) use ($groups) {
return $item->merge(['children' => $this->buildTree($groups, $item->id)]);
});
}
什么时候该放弃 buildTree() 改用 spatie/laravel-nested-set
不是功能强就该上,而是当业务动作开始“动”树结构时,buildTree() 就从工具变成瓶颈。它适合读多写少、层级浅、总量小(
- 移动一个节点,邻接表方案要更新该节点所有后代的
parent_id,N 层就要 N 次 UPDATE -
spatie/laravel-nested-set用lft/rgt字段,移动只需改 4 个数字,性能稳定 - 它自带
isDescendantOf()、getSiblings()等方法,不用自己拼whereRaw('path LIKE ?') - 迁移成本明确:运行
php artisan vendor:publish --provider="Spatie\NestedSet\NestedSetServiceProvider" --tag=migrations,再php artisan migrate
用 laravel-adjacency-list 做 CTE 递归查询要注意兼容性
这个包本质是帮你生成 MySQL 8.0+ 或 PostgreSQL 的 WITH RECURSIVE SQL,不是纯 PHP 方案。它能一次查出带 depth、path、is_cycle 的完整子树,但前提是你的数据库版本撑得住。
- MySQL 必须 ≥ 8.0,5.7 不支持
WITH RECURSIVE,强行用会报SQLSTATE[42000]: Syntax error - 开启 cycle detection 后,查询会自动加
CYCLE子句并返回is_cycle字段,但需确保模型中定义了getPathKeyName(),否则路径解析失败 - 它不替代数据校验:DB 层仍可能存入循环数据(如 A→B→C→A),CTE 只负责查的时候发现并中断,不阻止写入
真正容易被忽略的是:CTE 查询结果是扁平集合,不是嵌套对象。你得自己按 depth 或 path 再组织一次,或者前端直接消费扁平结构 + CSS 缩进。











