yii2处理唯一索引冲突应优先在模型层添加unique验证器,其次控制器捕获integrityexception,必要时改用insert on duplicate key update或on conflict语法,三者字段与顺序须严格一致。

Yii2 更新数据时遇到唯一索引冲突,不能靠 save() 硬扛——它会直接抛出 IntegrityException 导致 500 错误。必须在模型层或控制器层主动拦截、识别并转为用户友好的提示。
模型层加 unique 验证器(最推荐)
这是 Yii2 官方推荐且最稳定的方案,利用框架内置验证机制提前拦截,不依赖数据库报错。
- 验证发生在
save()前,不会触发 SQL 冲突,无性能损耗 - 自动适配 MySQL/PostgreSQL/SQL Server 的不同错误码,无需手动捕获
- 错误信息可直接绑定到字段,前端渲染自然友好
示例(User 模型中):
public function rules()
{
return [
// 其他规则...
['email', 'unique', 'message' => '该邮箱已被注册,请换一个'],
['phone', 'unique', 'targetAttribute' => ['phone', 'tenant_id'], 'message' => '该手机号在当前租户下已存在'],
];
}
注意:targetAttribute 必须与数据库中唯一索引的列完全一致(包括顺序),否则验证失效。
控制器层捕获 IntegrityException(备用兜底)
当模型验证无法覆盖全部场景(如复合唯一索引跨表、动态条件等),需在控制器中捕获异常并分类处理。
- 只捕获
\yii\db\IntegrityException,避免吞掉其他数据库错误 - 用
$e->errorInfo[1]取 MySQL 错误码1062,比字符串匹配更可靠(不同版本提示文本可能变) - PostgreSQL 应检查
$e->errorInfo[2] === '23505'
示例(SiteController::actionUpdate()):
try {
if (!$model->save()) {
throw new \yii\base\Exception('模型验证失败');
}
} catch (\yii\db\IntegrityException $e) {
$errorCode = $e->errorInfo[1] ?? null;
if ($errorCode == 1062) { // MySQL 唯一冲突
$model->addError('email', '该邮箱已被占用');
} else {
\Yii::error($e->getMessage(), 'db.integrity');
$model->addError('email', '操作失败,请稍后重试');
}
}
更新时想“覆盖旧值”?别用 UPDATE,改用 INSERT ON DUPLICATE KEY UPDATE
Yii2 的 updateAll() 或原生 UPDATE 语句无法绕过唯一约束检查。如果业务逻辑本质是“用新值覆盖旧记录(哪怕邮箱变了)”,应改用数据库原生 UPSERT 语法。
- MySQL:必须用
INSERT ... ON DUPLICATE KEY UPDATE,Yii2 不支持该语法直接生成,需手写 SQL 或使用createCommand() - PostgreSQL:必须显式指定
ON CONFLICT (col)或约束名,不能省略目标 - 注意:这会跳过 Yii2 模型事件(如
beforeSave),需自行补全逻辑
示例(MySQL 手写):
\Yii::$app->db->createCommand("INSERT INTO user (id, email, name) VALUES (:id, :email, :name)
ON DUPLICATE KEY UPDATE email = VALUES(email), name = VALUES(name), updated_at = NOW()")
->bindValue(':id', $id)
->bindValue(':email', $newEmail)
->bindValue(':name', $name)
->execute();
真正容易被忽略的是复合唯一索引的验证一致性:模型里 unique 规则的 targetAttribute、数据库索引定义、以及手写 UPSERT 的 ON CONFLICT 目标,三者必须完全对齐,差一个字段或顺序错位,就会导致验证通过但 SQL 报错,或验证失败但实际能插入。











