eloquent的hasmany无法实现无限级分类,因其仅支持单层关联查询,需手动递归导致n+1问题及栈溢出;应采用全量查询+php递归组装或nested set方案。

为什么直接用 Eloquent 的 hasMany 做不了无限级分类
因为 hasMany 只能查一级子类,无法自动展开所有后代节点。你写 $category->children,它只返回直属子项;要拿到孙子、曾孙……就得手动递归查库,不加控制会触发 N+1 查询,页面直接卡死。
常见错误现象:Maximum function nesting level of '256' reached(递归太深)、页面加载超时、数据库连接耗尽。
- 别在 Blade 模板里写
@foreach($item->children as $child) @include('child') @endforeach这种嵌套 include —— 每次 include 都可能触发新查询 - 别在模型的 accessor 里调用
$this->children再递归取值 —— 容易形成隐式无限循环 - 父子关系字段名必须统一,比如都叫
parent_id,且允许为NULL(根节点)
用 with + 递归集合处理一次性查出整棵树
核心思路:先用 Eloquent 查出全部分类,按 parent_id 归组,再用 PHP 递归组装树结构。避免多次查询,也绕过 Eloquent 关系的深度限制。
假设模型是 Category,表有 id、name、parent_id:
// 控制器中
$categories = Category::all()->groupBy('parent_id');
<p>function buildTree($groups, $parentId = null) {
$items = $groups->get($parentId, collect());
return $items->map(function ($item) use ($groups) {
return $item->merge(['children' => buildTree($groups, $item->id)]);
});
}</p><p>$tree = buildTree($categories);这个方案性能好、可控性强,但注意:$groups 是 Laravel 的 Collection,不是数组;merge 会新建对象,如果模型里有大量属性或访问器,建议用 only() 提前精简字段。
用 spatie/laravel-nested-set 处理高频读写场景
当分类需要频繁移动节点、拖拽排序、或者数据量超过 5000 条时,上面的“查全表再组装”就吃力了。这时候该上嵌套集模型(Nested Set)。
安装后执行迁移:php artisan vendor:publish --provider="Spatie\NestedSet\NestedSetServiceProvider" --tag=migrations,然后加字段 lft、rgt、depth。
- 必须在模型中使用
Spatie\NestedSet\Nodetrait,并设置$table和protected $guarded = [] - 插入新节点不能直接
save(),得用$parent->appendNode($new)或$node->makeRoot() - 查整棵树只需一条语句:
Category::defaultOrder()->get(),结果自带层级和顺序 - 不要手动改
lft/rgt值 —— 会破坏树结构,要用rebuild()修复
前端渲染树状结构时怎么传数据最省事
Blade 里遍历深度不确定的树,别手写多层 @if 判断是否有 children。推荐两种方式:
方式一:把树转成扁平数组带层级标识,传给前端 JS 渲染(适合 Vue/React):
$flattened = $tree->flatMap(fn($item) => [
$item->only(['id', 'name', 'depth']),
...collect($item['children'] ?? [])->flatMap(fn($c) => /* 递归展开 */)
]);方式二:在 Blade 中用递归组件(Laravel 9+ 支持):
<?php
// resources/views/categories/tree.blade.php
@props(['nodes'])
@foreach($nodes as $node)
<div style="padding-left: {{ $node['depth'] * 20 }}px">
{{ $node->name }}
@if($node->children->count())
<x-categories.tree :nodes="$node->children" />
@endif
</div>
@endforeach关键点:传入的 $nodes 必须是已经组装好的集合,不能每次进组件都重新查库;depth 字段如果是 Nested Set 方案就直接有,自己组装的树得在 buildTree 里手动加上。
最容易被忽略的是缓存策略:树结构变动不频繁,用 Cache::remember('categories-tree', 3600, fn() => buildTree(...)) 能省掉大部分重复计算。但一旦调用 save() 或移动节点,记得清掉这个 key。










