findOne后直接改属性再save易出错:须判空防null报错;字段需在rules中声明且合法才能更新;推荐用find()->where()->one()替代带条件的findOne;时间戳等需手动赋值或重写beforeSave;失败必须检查getErrors()。

findOne 查到对象后直接改属性再 save,是最常用也最容易出错的更新路径——它看似简单,但漏掉判空、绕过验证、忽略脏检查,都会让数据静默失败或覆盖错误。
findOne 返回 null 时调用 save 会报错
Yii2 的 findOne() 在查不到记录时返回 null,不是空对象。直接在未判空情况下调用 save() 会触发 PHP 致命错误:Call to a member function save() on null。
- 必须先判断结果是否为有效对象:
if ($user = User::findOne($id)) { ... } - 不要写成
$user = User::findOne($id); $user->save();这类无保护调用 - 尤其在 API 接口里,ID 来自 URL 或参数时,更要默认按“可能不存在”来处理
save 前没改属性,或改了不合法字段,会静默跳过更新
save() 默认只更新「被修改过」且「在 rules 中声明为安全」的字段。如果字段没出现在 rules() 里(比如数据库有 updated_by 但模型没加),即使你写了 $user->updated_by = 123,save() 也不会把它写进 SQL。
- 检查模型的
rules()方法,确保要更新的字段在数组中且类型匹配(例如tinyint字段规则写成'integer',不能写'string') - 想绕过安全校验批量赋值,可用
$user->load(['User' => $data], '')或直接操作$user->attributes = $data,但务必确认来源可信 - 更新后检查返回值:
if (!$user->save()) { var_dump($user->getErrors()); },否则失败无声
用 find()->where()->one() 替代 findOne() 更可控
当需要带额外条件(如只更新 status=1 的用户),别硬套 findOne(['id' => $id, 'status' => 1]) —— 它底层仍走主键索引优化,status 条件可能被忽略,或在某些 MySQL 版本下行为不一致。
- 明确用查询构造器:
$user = User::find()->where(['id' => $id, 'status' => 1])->one(); - 这样生成的 SQL 是完整 WHERE,且可链式追加
andWhere()、with()等 - 若需同时预加载关联数据(比如更新用户时一并拿到其订单),
find()->with('orders')->where(...)->one()比先findOne()再访问$user->orders更安全,避免懒加载触发 N+1
更新时间戳等字段要主动赋值
Yii2 不自动维护 updated_at 或 updated_by 这类字段,除非你在模型里重写了 beforeSave() 并手动设置。
- 常见做法是在模型中加逻辑:
public function beforeSave($insert) { if (!$insert) { $this->updated_at = date('Y-m-d H:i:s'); $this->updated_by = \Yii::$app->user->id ?? 0; } return parent::beforeSave($insert); } - 如果不想改模型,就在
save()前直接赋值:$user->updated_at = date('Y-m-d H:i:s'); - 注意:MySQL 的
TIMESTAMP类型字段若设了ON UPDATE CURRENT_TIMESTAMP,数据库层会自动更新,但 PHP 层读不到这个新值,$user->updated_at仍是旧值
最常被跳过的其实是 getErrors() 调试环节——save 失败不打印错误,就永远不知道是规则拦截、外键冲突,还是数据库字段长度超限。


















