用hasMany+belongsTo实现无限分类本质是自关联,Category模型需定义children(hasMany)和parent(belongsTo+withDefault)两个关系,配合withChildren递归加载或path路径字段优化查询性能。
hasMany + belongsTo 实现父子关联结构无限分类本质是「自关联」,Laravel 中最直接的方式是让模型同时定义子集关系和父级关系。假设模型叫 Category,数据库有 id、name、parent_id(允许为 NULL)三个关键字段。
在 Category 模型里写两个关联方法:
public function children()
{
return $this->hasMany(Category::class, 'parent_id');
}
public function parent()
{
return $this->belongsTo(Category::class, 'parent_id')->withDefault();
注意:withDefault() 能避免根节点调用 $category->parent->name 时抛出 Trying to get property 'name' of non-object 错误。
children() 用于向下查子类,支持链式调用如 $category->children()->with('children')
parent() 用于向上查父类,建议加 withDefault() 防空指针parent_id 加外键约束(除非你确定所有数据都严格符合树结构),否则软删除或临时断开关系时容易报错不依赖第三方包,用 Eloquent 的 with + 递归加载即可一次性取出整棵树。适用于层级不深(一般 ≤ 5 层)、数据量不大(几百条以内)的场景。
例如获取全部根节点及其子孙:
$tree = Category::whereNull('parent_id')
->with('children.children.children') // 手动展开 3 层
->get();
但硬编码层数不灵活。更通用的做法是封装一个递归加载器:
public function scopeWithChildren($query)
{
return $query->with(['children' => fn ($q) => $q->withChildren()]);
}
然后调用:
$tree = Category::whereNull('parent_id')->withChildren()->get();
SoftDeletes,记得在 children() 关联里加 withTrashed() 或过滤逻辑,否则被软删的中间节点会导致树断裂Blade 中递归渲染树最容易出问题的是:模板自己调自己导致无限循环,或没控制层级导致页面卡死。
推荐做法是在控制器中把数据转成扁平化带层级标识的数组,再传给视图;或者在 Blade 中用带深度参数的子视图。
例如建一个 resources/views/categories/tree.blade.php:
@props(['categories', 'depth' => 0])@foreach($categories as $category)
{{ $category->name }} @if($category->children->isNotEmpty()) @include('categories.tree', ['categories' => $category->children, 'depth' => $depth + 1]) @endif @endforeach
在主视图中调用:
@include('categories.tree', ['categories' => $tree])
$depth 并做递归限制(比如加 @if($depth 判断),防止意外形成环形引用崩溃
$category->children,必须提前用 with 加载好,否则变 N+1 查询 下拉菜单,建议用 collect($tree)->toTree()(需手动实现)或转成带前缀的扁平数组,比递归渲染更可控当分类数超千、层级深、频繁查询祖先/后代时,Eloquent 递归很快成为瓶颈。这时应放弃纯关系模型,改用「路径存储」方案:在表中加一个 path 字段(如 "0001/0005/0023"),用字符串前缀匹配快速定位上下文。
典型操作示例:
where('path', 'like', '0001/0005/%')
whereRaw("path REGEXP '^([0-9]+/)*0005$')"(或拆分后 in 查询)path 字段,无需递归更新关系缺点是写操作变重、路径长度需预估、迁移成本高。但读多写少的后台管理场景下,这是最稳的选择。
真正麻烦的不是怎么实现,而是前期没想清楚:这个分类会不会被前端频繁拖拽
排序?会不会导出到其他系统?要不要支持多语言别名?这些需求一旦出现,光靠 parent_id 就撑不住了。