必须配置 withdefault() 防止空指针,hasmany 外键参数填子表外键字段名(如 'parent_category'),查后代推荐 path 字段方案,按 type 过滤需用 load() 而非 with()。

直接定义 hasMany 和 belongsTo 就能跑通父子关系,但字段名、外键约束、空值处理这三点不注意,很快就会遇到 Trying to get property 'name' of non-object 或查询结果为空。
为什么 $category->parent 报错 "non-object"
根节点的 parent_id 是 null,调用 $category->parent->name 时 PHP 会尝试访问 null->name。Laravel 默认不会自动兜底。
- 必须在
parent()关联里加withDefault(),比如:return $this->belongsTo(Category::class, 'parent_id')->withDefault(); - 如果业务允许缺省值,还可以传数组:
withDefault(['name' => '未分类']) - 别依赖模型的
$casts或getXXXAttribute处理这个场景——那是字段级逻辑,不是关联兜底
hasMany 的外键参数到底填什么
hasMany 第二个参数不是“当前模型的主键”,而是“子表里指向父表的外键字段名”。很多人误写成 id,导致查不到子数据。
- 假设数据库字段是
parent_category,那就写:$this->hasMany(Category::class, 'parent_category') - 第三个参数(
$localKey)才是当前模型的主键,默认是id;只有当你改过主键名(比如叫cat_id),才需要显式传入:->hasMany(Category::class, 'parent_category', 'cat_id') - 别在迁移里给
parent_category加外键约束(foreignId('parent_category')->constrained()),软删除或临时断开树结构时容易触发数据库报错
想查某类别的全部后代(递归子集)怎么办
Eloquent 原生不支持无限递归查询,硬套 with('children.children.children') 既难维护又容易 N+1。真要一次性拉完整棵树,得靠路径字段或闭包表。
- 最轻量方案:加一个
path字段(如/1/5/12/),用whereRaw("path LIKE ?", [$category->path . '%'])查后代 - 如果已有数据且不能改结构,可以用
whereHas嵌套,但最多撑 3 层,再多就卡顿 -
withChildren是社区封装的宏,本质还是递归加载,没解决查询次数问题;生产环境慎用
按 category_type 区分父子类型时怎么写关联
业务中常要求“主类目只挂上级类目,上级类目再挂次级类目”,这时不能只靠一张表 + 一个外键,得用带条件的关联方法。
- 定义专用关系:
superiorChildCategories()返回category_type = 2的子集,内部用->where('category_type', 2) - 反向也一样:
mainParentCategory()在belongsTo后加->where('category_type', 1) - 注意:这种带
where的关联不能用于with()预加载——Eloquent 会忽略 where 条件,必须用load()显式调用
父子关系看着简单,但外键字段名、withDefault()、path 字段、类型过滤这四个点,任何一个漏掉都会让树形数据在某个边界场景下突然失效。尤其是 withDefault(),它不是可选项,是防止空指针的底线配置。











