yii activerecord 默认禁止更新主键字段,save() 会静默跳过主键;如需修改,必须用 updateall() 手动处理,并同步更新外键、校验冲突、清理缓存。

Yii ActiveRecord 更新主键字段时 save() 静默失败或报错
Yii 的 ActiveRecord 默认禁止更新主键字段,这不是 bug,而是设计约束。当你对已存在的模型实例修改 $model->id 后调用 save(),框架会跳过该字段(即使你在 rules() 里没设 safe),导致看似“成功”但数据库值未变;更糟的是,若强行绕过(如用 updateAll()),可能破坏外键关联或触发唯一冲突。
- 主键字段在
save()中默认被ActiveRecord::scenarios()的'default'场景排除,除非显式加入safe规则 - 即使加了
['id', 'safe'],save()仍不会生成UPDATE ... SET id = ?SQL —— 它只更新非主键字段 - 想真正改主键,必须用
updateAll()手动构造 SQL,并自行处理外键、索引、缓存一致性等副作用
用 updateAll() 强制更新主键字段的正确写法
如果你确认业务需要改主键(比如合并旧记录、迁移历史 ID),唯一可靠路径是绕过 AR 生命周期,直连 Query Builder。
- 先确保目标新主键值在表中不存在:
SELECT COUNT(*) FROM user WHERE id = 'new_id' - 用
updateAll()更新自身主键:User::updateAll(['id' => 'new_id'], ['id' => 'old_id']) - 同步更新所有引用该主键的外键表(如
post.user_id):Post::updateAll(['user_id' => 'new_id'], ['user_id' => 'old_id']) - 清空相关 AR 查询缓存(如有):
Yii::$app->cache->delete('user_old_id')
注意:updateAll() 不触发 beforeSave/afterSave,也不校验数据完整性 —— 这些都得你手动补。
UUID 主键场景下误改 _id 的典型陷阱
在 MongoDB 或 UUID 主键的 MySQL 表中,有人试图在 beforeSave() 里重写 $this->_id 或 $this->id,结果插入重复、查询丢失。根本原因是:主键生成逻辑和主键识别逻辑必须对齐。
- MongoDB 模型必须返回
['_id'](不是['id'])给primaryKey(),否则findOne()根本不查_id字段 - MySQL + UUID 场景下,若用
UuidBehavior,它默认只在INSERT时填充,UPDATE时不会重写id—— 即使你手动赋值,save()也会忽略 - 真要换 UUID 主键,流程只能是:
INSERT new record→UPDATE related rows→DELETE old record,而非原地改
为什么 isNewRecord 不能用来判断主键是否可更新
$model->isNewRecord === false 只表示该对象曾调用过 save() 并未失败,不代表数据库里真有这条记录(比如事务回滚后)。直接基于它决定“能否改主键”,会误判。
- 正确做法是:先用
findOne()查原始主键是否存在,再查新主键是否冲突 - 例如:
User::findOne(['id' => $oldId]) !== null && User::findOne(['id' => $newId]) === null - 别依赖
isNewRecord做主键变更决策 —— 它不反映数据库真实状态
主键不是普通字段,改它等于重定义数据身份。Yii 的沉默处理不是疏漏,是防止你掉进外键断裂、缓存错乱、关联失效的坑里。真要动,就得全链路自己兜底。











