需要明确的是,ThinkPHP关联模型并没有提供所谓的“自动写入/更新”魔法开关。所谓的“自动”功能,实际上都需要开发者手动编写配置逻辑才能生效。核心原则在于:主模型和从模型必须分开独立处理,时间戳字段和业务字段需依靠修改器或钩子接管;批量操作则要规规矩矩地绕过模型逻辑来执行——只有理解透彻这些要点,才能避免静默失败或数据不一致等问题。

一对一关联的写入与更新操作
对于hasOne和belongsTo这类一对一关联,框架支持直接写入数据,但需要满足几个前提条件:
- 主模型必须先存在——只有主键存在后,才能写入关联记录。新建主模型后,务必先调用一次
save()成功入库,再调用关联的save()。 - 关联方法返回的是一个单独的模型对象,比如
$user->profile(),而不是集合对象,因此可以安全调用save()。 - 直接传入数组即可:
$user->profile()->save(['email' => 'a@b.com']),框架会自动补全user_id字段。 - 有一个容易踩的陷阱:如果关联记录还不存在,
$user->profile会返回 null,此时直接调用save()会报错。正确做法是改用$user->profile()->create([...]),或者先判断是否为空再创建新记录。
一对多关联不能直接用 save() 处理
hasMany 或 belongsToMany 返回的是一个 Collection 对象,该对象本身没有 save() 方法。常见的错误写法是 $user->comments->save(),这样会直接引发错误。
- 正确的做法是:遍历每条数据,通过关联关系对象逐条保存,例如
$user->comments()->save($comment)。 - 千万不要为了方便直接使用
Db::table()->insertAll()手动补外键——这样做会绕过验证、事件、时间戳等模型逻辑,容易引发问题。 - 如果对性能要求较高,可以考虑使用
Comment::insertAll($data),但此时必须显式传入user_id,而且关联生命周期不会被触发。
关联字段联动:完全依赖修改器,不能指望自动完成
例如状态码转文字、金额存整数、登录用户ID自动写入等需求,已经不能依赖旧版的 $_auto 属性——TP6 已彻底移除该特性。必须使用修改器(Mutator)来实现:
- 如果字段名是
status_text,就定义setAttrStatusText()方法,在写入之前对值进行加工处理。 - 如果一个字段的值依赖其他字段,比如 status 改变时需要同步更新 status_text,可以在
setAttrStatus()中同时处理两个字段,注意避免递归调用。 - 派生字段最好设置为只读:
protected $readonly = ['status_text'],防止外部不小心覆盖该字段。 - 创建者和更新者ID的处理思路相同:在
setCreateUserIdAttr()和setUpdateUserIdAttr()中调用Auth::id()实现自动填充。
批量更新和复杂条件:规规矩矩绕过模型操作
saveAll() 不会走模型生命周期,所有修改器、事件、时间戳都会失效;而 where()->update() 也不会触发任何模型逻辑。
- 需要批量更新关联表时,例如给多个用户的 profile 统一修改数据,直接使用
Db::table('profile')->whereIn('user_id', $ids)->update([...])。 - 如果更新条件涉及关联关系,比如要将“所有认证过的用户状态改为2”,必须手动编写 join 查询:
Db::table('user')->alias('u')->join('profile p', 'u.id = p.user_id')->where('p.verified', 1)->update(['u.status' => 2])。 - 遇到涉及计算或 JSON 字段更新的场景,例如
score = score + 10或JSON_SET(config, '$.theme', 'dark'),必须借助Db::raw()才能实现。
