直接用 save() 会强制更新所有字段,需先查出原记录触发 afterFind 以填充 oldAttributes,再调用 getDirtyAttributes() 获取真实变更字段并用 updateAttributes() 更新;若跳过验证和钩子,可用原生命令直写 SQL。

直接用 save() 会强制写入所有指定字段,哪怕值没变;想跳过未变更字段、只更新真正改了的内容,得绕过 ActiveRecord 的默认行为。
为什么 $model->save() 总是执行 UPDATE
ActiveRecord 默认不比较原始值和新值,save() 只看属性是否被赋值、是否通过验证,然后拼出完整 UPDATE 语句——哪怕 $model->name 原来就是 'tom',再设一次 'tom',照样发 SQL。
- 底层调用
updateAll()时传的是全字段数组,不判断脏数据 -
getDirtyAttributes()在没显式标记为“已加载”时可能返回空,导致误判 - 即使加了
$model->setOldAttribute('name', 'tom'),若没触发afterFind或手动初始化旧值,依然无效
正确获取并只更新脏字段
必须显式加载旧值,并用 getDirtyAttributes() 提取真实变更项。这步不能省,否则永远拿不到差异。
- 先查出原记录:
$model = User::findOne($id)(触发afterFind,自动存旧值) - 修改部分属性:
$model->email = 'new@example.com'; $model->status = 2; - 调用
$dirty = $model->getDirtyAttributes();→ 返回['email' => 'new@example.com', 'status' => 2] - 仅更新脏字段:
$model->updateAttributes($dirty);(底层走updateAll(),只拼这两个字段)
避免 save() 触发验证和钩子的轻量写法
如果连 beforeSave、afterSave 都不想跑,又确定数据可信,就彻底跳过模型层。
- 用原生命令直写:
Yii::$app->db->createCommand()->update('user', ['status' => 2], ['id' => $id])->execute(); - 注意:该方式不走任何规则、场景、钩子,也不校验字段是否存在,错写字段名会静默失败
- 批量更新多个 ID 时,条件可写成
['in', 'id', [1,2,3]],比循环save()快一个数量级
容易忽略的关键点
getDirtyAttributes() 依赖 $model->oldAttributes,而它只在 find() 类方法或 refresh() 后存在;直接 new Model() 再 set 属性,oldAttributes 是 null,getDirtyAttributes() 永远返回空数组——这点在定时任务或 API 批量处理中极易踩坑。


















