save() 通过 isnewrecord 属性判断执行 insert 还是 update:new 创建时为 true,查询获取时为 false;手动赋主键不改变该值,反序列化可能丢失此状态;强制操作应使用 save(false)、update() 或先查后存。

save() 怎么知道该 insert 还是 update
save() 不靠 SQL 查询判断,也不看主键是否为空,它只看一个属性:isNewRecord。这个布尔值在 AR 实例生命周期里由框架自动维护。
- 用
new User()创建的对象,默认isNewRecord === true - 用
User::findOne(123)、User::find()->where(...)->one()等查出来的对象,默认isNewRecord === false - 手动设成
true或false会强制改变行为(但不推荐,容易出错)
常见错误现象:
- 新建对象后手动赋了主键(比如
$user->id = 100),但没调$user->setIsNewRecord(true),结果save()去执行UPDATE并报“找不到匹配记录” - 从缓存反序列化或数组重建的模型,
isNewRecord丢失,变成null或false,导致本该插入的变成了更新
所以关键不是“有没有 ID”,而是“这个对象是不是刚 new 出来的、还没落地过”。框架内部就是靠这个开关决定调用 insert() 还是 update()。
什么时候 isNewRecord 会被意外改掉
isNewRecord 是个易失状态,几个典型干扰点:
- 调用
$model->refresh()后,如果数据库里真有这条记录,isNewRecord会被设为false(哪怕你原本是 new 出来的) - 批量操作如
updateAll()、deleteAll()不走 AR 生命周期,不会影响任何实例的isNewRecord,但如果你之后拿这个模型再save(),行为就取决于它当前的值 - 用
attributes块赋值时,如果传入包含主键的数组(比如['id'=>5, 'name'=>'x']),框架不会自动设isNewRecord = false—— 它只认“怎么创建的”,不认“赋了什么值”
最容易踩的坑:在控制器里写 $user = new User(); $user->attributes = $postData;,然后直接 $user->save(),结果因为 $postData 里带了 id 字段,数据库报主键冲突或静默覆盖旧数据 —— 因为 isNewRecord 还是 true,它仍走 INSERT,而 MySQL 报 Duplicate entry。
想强制走 insert 或 update 怎么办
别硬改 isNewRecord,用更明确的方式:
-
强制插入(忽略主键冲突):
$user = new User(); $user->name = 'xxx'; $user->save(false); // false 表示跳过验证,但仍是 insert
如果要确保插入且不怕主键重复,得配合ON DUPLICATE KEY UPDATE,这时就得绕过save(),用createCommand()->insert()或原生 SQL。 -
强制更新(即使它是 new 出来的):
$user = new User(); $user->id = 123; $user->name = 'yyy'; $user->update(false, ['name']); // 第二个参数指定只更新 name 字段
注意:这不会触发beforeSave/afterSave,也不会校验规则(除非第一个参数为true)。 -
更安全的“先查后更”模式:
$user = User::findOne($id) ?? new User(); $user->name = 'zzz'; $user->save();
这样逻辑清晰,isNewRecord自然准确,也符合大多数业务场景。
调试时怎么看当前是 insert 还是 update
最直接的办法是在 beforeSave 里打日志:
public function beforeSave($insert)
{
\Yii::info('save mode: ' . ($insert ? 'insert' : 'update'), __METHOD__);
return parent::beforeSave($insert);
}
或者临时加一行:
var_dump($this->isNewRecord, $this->getOldPrimaryKey());
getOldPrimaryKey() 返回的是查询时读到的原始主键值(仅对已加载的记录有效),和 isNewRecord 配合看,能快速定位状态是否异常。
真正复杂的地方在于:这个判断发生在内存层面,和数据库实际是否存在无关。如果你用 findOne() 查不到数据,返回 null,那后续根本没机会调 save();但如果你用 find()->where(...)->one() 没命中,却误以为拿到了对象,再强行 save(),就会因 isNewRecord === false 而尝试更新一条不存在的记录 —— 此时 MySQL 报 Affected rows: 0,但 PHP 不报错,save() 返回 true,极易漏掉问题。











