parent_id 字段必须设为 nullable 且使用 unsignedbiginteger 类型,配合 nullondelete() 而非 cascade,并在模型中正确定义 children() 和 parent() 关联并调用 withdefault(),才能正确构建树形结构。

parent_id 字段必须 nullable,否则根节点插不进去
根节点的 parent_id 必须为 null,这是树形结构成立的前提。如果迁移里写成 $table->foreignId('parent_id')->constrained()(没加 ->nullable()),插入第一条分类就会报 Integrity constraint violation。
正确写法是:
$table->foreignId('parent_id')->nullable()->constrained()->nullOnDelete();
注意三点:
-
->nullable()不可省,否则NULL值被拒绝 -
->nullOnDelete()比->onDelete('cascade')更安全,父删后子不会丢,而是自动置空 - 别加外键约束到自身表(MySQL 8.0+ 支持,但 Laravel 迁移生成的
constrained()默认指向同名表,容易循环依赖)
模型里 children() 和 parent() 必须成对定义
只写 children() 或只写 parent() 都是半残状态:前者查不了上级,后者查不了下级;漏掉 withDefault() 会导致根节点调用 $category->parent->name 时直接抛出 Trying to get property 'name' of non-object。
标准定义如下:
public function children()
{
return $this->hasMany(Category::class, 'parent_id');
}
public function parent()
{
return $this->belongsTo(Category::class, 'parent_id')->withDefault();
}
关键点:
- 两个方法都显式传入
'parent_id',避免 Laravel 自动推导出错(尤其当字段名非标准时) -
parent()一定要链式调用withDefault(),否则空关系返回null,后续取属性必崩 - 别用
hasOne替代belongsTo—— 后者才是“本记录属于哪个父记录”的语义,前者是“父记录有且仅有一个本记录”,逻辑反了
查整棵树别手写递归,用 with + whereNull 预加载
手动写 PHP 递归查 N 层、或用 foreach 嵌套查子集,性能差还容易爆内存。Eloquent 原生支持多层预加载,只要层级不深(≤5)、数据量不大(几百条),直接用 with() 最稳。
例如获取全部根节点及其子孙:
$tree = Category::whereNull('parent_id')
->with('children.children.children')
->get();
注意事项:
- 层数写死(如
children.children.children)适合已知深度的场景;若深度不确定,改用scopeWithTree()封装动态加载逻辑 - 不要在循环里反复调用
$category->children—— 这会触发 N+1 查询,哪怕加了with()也没用 - 预加载的 key 必须和模型里定义的关联方法名完全一致,大小写敏感,比如写了
childCategories(),就不能用with('children')
想按类型过滤父子关系?加 where 条件到关联里
业务中常需“只查 type=2 的子类目”或“只往上找 type=1 的主类目”,这时不能靠 PHP 过滤,得把条件下沉到 SQL 层,否则拉回全量再筛,浪费 IO 和内存。
示例:定义一个只返回 category_type = 2 的子分类关联:
public function superiorChildCategories()
{
return $this->hasMany(Category::class, 'parent_category', 'id')
->where('category_type', 2);
}
使用时一样能预加载:
$categories = Category::whereNull('parent_category')
->with('superiorChildCategories')
->get();
注意:
- 这种带条件的关联不能和无条件的
children()混用,否则 Eloquent 会因关系名冲突报错 - 如果要同时查多种类型子集,建议统一用
category_type字段 + 多个命名关联,而不是在控制器里用filter()处理集合 - 数据库里
category_type字段记得加索引,否则where条件会全表扫
parent_id 字段的类型和约束组合——它既要支持 null,又得是整数型(unsignedBigInteger),还得避开外键自引用陷阱。这三者少一个,树就建不起来。











