$model->save() 会插入新记录而非更新,是因为主键缺失或无效:post未传id、字段name未包含主键、load()未正确调用、id类型转换失败(如字符串未转整型)、数据库int字段接收空字符串导致id为0或空,均被activerecord识别为“无主键”而执行insert。

直接用 $model->save() 就行,但必须确保模型已正确载入数据、通过验证、且主键存在(更新场景)——否则它会变成插入新记录。
为什么 $model->save() 有时插了新数据而不是更新?
这是最常踩的坑:ActiveRecord 默认行为是「有主键值就 update,没主键值就 insert」。如果你从 POST 拿到的数据里没传 id,或者 id 被过滤/丢失/类型转换失败(比如字符串 "123" 没转成整型),$model->save() 就会当成新增处理。
- 检查视图中字段 name 是否包含主键,例如:
<input name="User[id]">;漏掉这行,load()就不会给$model->id赋值 - 确认控制器里调用了
$model->load($post),而不是只赋部分属性:$model->name = $post['name']—— 这样主键永远不会被设上 - 数据库字段为
INT NOT NULL,但 POST 传的是空字符串或null,PHP 类型弱转换后$model->id变成0或'',ActiveRecord 认为“无主键”,触发 insert
load() 和 setAttributes() 的关键区别
load() 会按表单字段命名规则(ModelName[attribute])自动提取并赋值,同时支持批量安全赋值(scenarios 和 rules 控制);setAttributes() 是裸赋值,绕过所有规则校验和字段白名单。
- 更新操作必须用
$model->load(\Yii::$app->request->post()),不能只写$model->setAttributes($_POST['User'])—— 后者不触发scenarios切换,可能跳过必填校验 - 如果 POST 数据结构不是标准嵌套格式(比如来自 JSON 接口),改用
$model->load($data, ''),第二个参数留空表示不加前缀 -
load()返回布尔值:成功返回true,失败(如无匹配字段)返回false,务必检查这个返回值再走validate()
更新时要不要手动查一遍再改?
绝大多数情况不需要。ActiveRecord 的 save() 内部会先做 SELECT(仅当启用 optimisticLock 或使用 updateAll() 时才跳过),但更关键的是:你得决定是否要保留原始状态、触发钩子、走验证流程。
- 只想改几个字段且不关心验证/钩子?直接用
User::updateAll(['status' => 1], ['id' => $id]),快、轻、绕过模型生命周期 - 需要完整验证 + 触发
beforeSave/afterSave?必须走find()->one()+load()+save()流程 - 担心并发覆盖?在模型里定义
optimisticLock()并在表中加version字段,save()会自动带上WHERE version = ?条件
真正容易被忽略的点是:save() 成功只代表 SQL 执行成功,不代表业务逻辑成立——比如外键关联的记录已被删,或唯一索引冲突发生在事务提交瞬间。这些错误不会被 validate() 捕获,得靠 try/catch 包住 save() 并检查 $model->hasErrors() 和底层异常类型。











