必须先保存主模型获取ID再保存从模型,并用事务保证原子性。外键需手动赋值且名称严格匹配,inverseOf仅影响查询不影响保存。

关联模型 save() 之前必须先保存主模型并获取 ID
Yii 的 hasOne 或 hasMany 关系本身不自动保存从属记录,只是查询契约。想让 Profile 随 User 一起存入数据库,得手动控制顺序:先 $user->save() 成功拿到 $user->id,再把该 ID 赋给 $profile->user_id,最后调用 $profile->save()。
常见错误现象:$profile->save() 报错 “Integrity constraint violation: 1048 Column 'user_id' cannot be null”,就是因为跳过了主模型保存步骤,或没显式赋值外键。
- 外键字段名必须和数据库定义完全一致(比如是
user_id,不是userId或owner_id) - 如果主模型使用自增主键,
$user->id在save()后才可用;若用 UUID 或业务主键,需确保它在save()前已设好 - 别依赖关联对象的 magic 属性反向赋值(如
$user->profile->user_id = $user->id),应直接操作从模型实例
用 transaction 包裹多模型保存防止数据不一致
主表和从表写入是两个独立 SQL 操作,中间若出错(如网络中断、验证失败、磁盘满),会导致只存了用户没存资料,或反之。必须用 Transaction 保证原子性。
示例关键代码:
$transaction = Yii::$app->db->beginTransaction();
try {
if (!$user->save()) {
throw new Exception('User save failed');
}
$profile->user_id = $user->id;
if (!$profile->save()) {
throw new Exception('Profile save failed');
}
$transaction->commit();
} catch (Exception $e) {
$transaction->rollBack();
throw $e;
}
- 事务不能跨 HTTP 请求生效,所有关联模型必须在同一个 action 内完成 load → validate → save 流程
- 别在事务里做耗时操作(如发邮件、调外部 API),否则锁表时间过长
- MySQL 默认隔离级别是 REPEATABLE READ,对多数场景够用;高并发写入可考虑调整为 READ COMMITTED
hasMany 场景下批量保存子记录要循环调用 save()
一对多(如 User → Post)新增时,没有内置的“一键保存全部子模型”方法。$user->posts 是一个 AR 对象数组,但它们不会因 $user->save() 自动落库。
必须显式遍历并逐个保存:
foreach ($posts as $post) {
$post->user_id = $user->id;
if (!$post->save()) {
throw new Exception('Post save failed: ' . json_encode($post->getErrors()));
}
}
- 不要用
batchInsert()替代单个save(),除非你放弃模型层验证、事件(beforeSave等)和类型转换 - 如果子记录数量大(>100 条),考虑拆成多个事务批次,避免单次事务过大导致锁表或超时
-
save()失败时记得立即中断循环并回滚,别继续往后执行
inverseOf 配错不影响保存,但会干扰 eager loading 和内存引用
inverseOf 只影响查询优化和对象间属性访问(如 $post->user 是否复用已查出的 $user),和保存逻辑完全无关。但它容易被误认为“关联没配好导致存不进数据”。
检查点:
- 确认从模型(如
Profile)里定义的反向方法名,和主模型中inverseOf()参数值严格一致(大小写、下划线) - 用
asArray()+with()打印生成的 SQL,看是否用了 JOIN 还是 N+1 子查询 - 如果只是单纯新增,
inverseOf配错顶多让$user->profile返回新实例而非已有对象,不影响数据库写入结果
真正卡住保存的,永远是外键赋值缺失、事务未提交、或验证规则拦住了 save() 返回 false —— 不是 inverseOf。


















