事务回滚时异常被吞会导致前端无提示,必须在catch中显式反馈:模型场景用adderror()挂错误、api场景设statuscode并返回json,且所有事务须由控制器统一管控,禁用内层嵌套事务。

事务回滚时异常被吞掉,前端看不到提示
Yii2 默认在 try...catch 中调用 $transaction->rollback() 后,如果没显式处理或重新抛出异常,错误就静默消失了。用户点击保存没反应,日志里也查不到原因,这是最常踩的坑。
关键点在于:事务回滚本身不抛异常,真正出错的是业务逻辑里的操作(比如数据库约束失败、模型验证不通过),但如果你在 catch 块里只写了 $transaction->rollback() 而没做任何输出或响应处理,那这个错误就彻底“消失”了。
- 必须在
catch块中显式向用户反馈,不能只 rollback - 不要用
print_r($e)或var_dump()—— 这些只输出到 PHP 输出缓冲,前端收不到 - 如果是 AJAX 请求,得返回 JSON 格式的错误信息;如果是表单提交,得把错误塞进模型再交给视图渲染
在控制器里用 $model->addError() 传递提示
适用于普通表单提交场景。事务失败后,把错误挂到模型上,让 ActiveForm 自动显示。
$transaction = Yii::$app->db->beginTransaction();
try {
$model->load(Yii::$app->request->post());
if (!$model->validate()) {
throw new \Exception('数据验证失败');
}
$model->save(false);
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollback();
// 关键:把错误挂到模型,而不是直接 echo 或 log
$model->addError('base', '保存失败:' . $e->getMessage());
}
视图里用 ActiveForm 就能自动显示:= $form->errorSummary($model) ?> 或字段级提示。
-
$model->addError('base', ...)表示非字段级通用错误 - 如果知道具体字段(比如
email冲突),可用$model->addError('email', '邮箱已被占用') - 别漏掉
return $this->render(...),否则页面不会刷新显示错误
API 接口返回 JSON 错误需手动设状态码
前后端分离项目中,事务失败必须返回明确的 HTTP 状态码和结构化错误体,否则前端无法区分是网络错误、401 还是业务失败。
try {
// ... 业务逻辑
$transaction->commit();
return ['code' => 0, 'msg' => '成功'];
} catch (\Exception $e) {
$transaction->rollback();
Yii::$app->response->statusCode = 400; // 或 500,按语义选
return [
'code' => -1,
'msg' => '操作失败,请稍后重试',
'debug' => YII_DEBUG ? $e->getMessage() : null, // 调试环境才暴露细节
];
}
- 别依赖框架自动设状态码 —— Yii2 的
ErrorHandler在 API 场景下默认仍会跳转到 HTML 错误页 -
YII_DEBUG是开关:生产环境绝不能返回$e->getTraceAsString()或原始 SQL 错误 - 如果用了自定义
ExceptionHandler类,确保它没覆盖掉你手动设置的statusCode和响应格式
事务内嵌套调用导致回滚失效
常见于多个模型方法各自调用 beginTransaction(),外层 rollback 无法影响内层已 commit 的操作,最终提示“看似成功实则部分失败”。
根本原因:Yii2 的 beginTransaction() 不支持真正的嵌套事务(MySQL 也不原生支持),第二次调用只是增加嵌套计数,rollback() 只减计数,只有最外层 rollback 才真回滚。
- 所有涉及事务的操作,必须由最外层控制器统一开启和控制,内部方法不要自己 begin/commit/rollback
- 内部方法应设计为“无副作用”,即只做数据准备,不执行 DB 写入;写入统一交由外层事务包裹
- 检查是否误用了
Yii::$app->db->createCommand()->execute()直接执行 SQL —— 它不参与当前事务,需改用$transaction->getMasterPdo()->prepare()等方式绑定











