Laravel 8.x 中基于 parent_id 实现无限级分类需用邻接表模型,关键点:parent_id 必须 nullable 并加索引;模型需同时定义 children() 和 parent() 关系;查树用闭包预加载或 PHP 递归组装;插入用递归事务函数。

在 Laravel 8.x 中基于 parent_id 实现无限级分类,核心是用「邻接表模型(Adjacency List)」配合 Eloquent 的自关联关系。它轻量、易理解、无需额外包,适合中小项目——但必须避开几个关键坑,否则查不出来、插不进去、一查就卡。
数据库字段设计要严谨
迁移文件中必须确保:
-
parent_id字段允许为 NULL(不能设default(0)或NOT NULL),根节点才可插入; - 使用
foreignId()并显式声明外键约束和级联删除; - 给
parent_id加数据库索引,否则深层查询会全表扫描。
示例迁移代码:
Schema::create('categories', function (Blueprint $table) {$table->id();
$table->string('name');
$table->foreignId('parent_id')->nullable()->constrained()->nullOnDelete();
$table->index('parent_id'); // 关键!
$table->timestamps();
});
模型里两个关系缺一不可
只写 children() 不够,只写 parent() 也不行。必须同时定义,且参数顺序不能错:
-
children():用hasMany,外键是parent_id,关联到本模型; -
parent():用belongsTo,外键也是parent_id,并加withDefault()防止访问空对象报错。
模型代码示例:
{
protected $fillable = ['name', 'parent_id'];
public function children()
{
return $this->hasMany(Category::class, 'parent_id');
}
public function parent()
{
return $this->belongsTo(Category::class, 'parent_id')->withDefault();
}
}
查整棵树要预加载,别用纯递归作用域
想一次性查出全部层级,with('children') 只展开一层;嵌套写 with(['children.children']) 最多两层。若需完整树结构,推荐两种安全做法:
- 用闭包预加载控制深度(比如最多三层):
Category::whereNull('parent_id')<br> ->with(['children' => fn($q) => $q->with(['children' => fn($q2) => $q2->with('children')])])<br> ->get(); - 若真要全深度,先用一次查询取全量
id → parent_id映射,再用 PHP 递归组装数组树(避免 N+1 和爆栈)。
插入嵌套数据要用递归函数,别硬写 foreach
前端传入的菜单或分类 JSON 是嵌套结构,插入时不能靠多层 for 循环。应封装一个递归插入方法,自动处理父子关系:
- 每插入一个节点,拿到它的新
id; - 把该
id作为parent_id传给下一层子节点; - 用事务包裹,失败则全部回滚。
示例逻辑片段(建议抽成 Service 类):
public function insertTree(array $nodes, ?int $parentId = null){
foreach ($nodes as $node) {
$category = Category::create([
'name' => $node['name'],
'parent_id' => $parentId,
]);
if (! empty($node['children'])) {
$this->insertTree($node['children'], $category->id);
}
}
}











