belongsTo和hasOne易混淆:User有category_id应使用belongsTo,Category有admin_id才用hasOne;with预加载失效主因是关联方法未return、非public或子模型未定义关联;join返回扁平数组,with返回嵌套对象;with加载的关联默认只读,需重新实例化或load才能保存。

belongsTo 和 hasOne 容易写反,查不到数据却不报错
最常踩的坑是把 belongsTo 和 hasOne 用混,比如 User 表里有 category_id 字段,却在 User 模型里写 public function category() { return $this->hasOne(Category::class); }。结果查出来 $user->category 是 null,但控制台没任何错误提示。
关键看「谁属于谁」:
- User 有
category_id→ User 属于 Category → 必须用belongsTo - Category 表里没有用户字段,但每条记录「拥有一个」管理员(比如
admin_id)→ Category 拥有一个 User → 才用hasOne -
belongsTo必须显式指定外键名:return $this->belongsTo(Category::class, 'category_id');,不写就默认找category_id,但换成cat_id就失效 -
hasOne默认按当前模型主键(如id)去关联,如果被关联表用的是sn字段,得加localKey:hasOne(User::class, 'rid', 'sn')
with 预加载写了却还是 N+1,原因基本就这三处
User::with('category')->select() 看似写了预加载,但抓包一看还是发了十几条 SQL——不是框架不行,而是配置漏了或写法错了。
- 关联方法没
return:方法体里必须以return $this->belongsTo(...)结尾,否则 ThinkPHP 反射调用时等于没定义 - 方法不是
public:protected function category()不会被with找到 - 嵌套层级里子模型没正确定义关联:比如
with(['category.profile']),那Category模型里必须有public function profile()且返回有效关联对象
验证是否生效,可以临时加一句 Db::listen(function ($sql) { dump($sql); }); 看实际执行了几条 SQL。
立即学习“PHP免费学习笔记(深入)”;
join 和 with 返回结构完全不同,选错后续全崩
join 是 SQL 层面拼表,with 是 PHP 层面做对象组装,二者根本不是替代关系,而是分工不同。
-
join返回一维数组:['id' => 1, 'name' => '张三', 'category_name' => '技术'],适合做where('category_name', 'like', '%技%')或order('category_name') -
with返回嵌套对象:$user->category->name,适合分页、序列化、模板渲染;但不能直接对$user->category->name做 where 条件 - 用
join查一对多时容易产生笛卡尔积,导致分页数不准;with先查主表再批量查关联,更稳 -
join结果不是完整模型实例,触发不了模型的afterFind事件,也调不了save()
关联数据改了 save 不生效,因为默认只读
$user = User::with('profile')->find(1); $user->profile->name = '新名字'; $user->profile->save(); 执行完数据库毫无变化——这不是 bug,是设计如此。
ThinkPHP 的 with 加载出来的关联对象,默认是只读快照,不绑定原始模型状态。要更新,只有两种办法:
- 重新实例化:
$profile = Profile::find($user->profile->id); $profile->name = '新名字'; $profile->save(); - 用
load()强制重载:$user->load('profile'); $user->profile->name = '新名字'; $user->profile->save();(前提是profile方法返回的是可写关联)
别指望 $user->profile->save() 自动同步,它连事务都不参与,改了也是白改。



















