Yii2事务未自动回滚的典型表现是异常抛出后数据库记录仍写入,主因包括未调用begin()、在事务外执行AR操作、连接被复用;正确做法是:显式beginTransaction()→try内执行所有同连接操作→catch中必须rollback()。

Yii2中事务未自动回滚的典型表现
执行 $transaction->commit() 前抛出异常,但数据库记录已写入——这说明事务没真正生效。常见原因包括:没调用 begin()、在事务块外调用了非事务性 ActiveRecord 操作(如直接调用 save() 但未传 $runValidation = false)、或底层连接被意外复用导致隔离失败。
正确开启并控制 Yii2 事务的三步写法
必须显式管理事务生命周期,不能依赖模型方法自动处理:
- 用
Yii::$app->db->beginTransaction()获取事务对象,而非ActiveRecord::getDb()->beginTransaction()(后者可能返回错误连接) - 所有涉及写操作的
save()、insert()、updateAll()等必须在try块内,并确保它们走的是同一个 DB 连接(检查是否误用了不同组件配置的 db 实例) -
catch中必须调用$transaction->rollBack(),且不能只靠finally—— 因为commit()成功后不能再 rollback,而未 commit 就抛异常时 rollback 是唯一补救手段
示例片段:
$transaction = Yii::$app->db->beginTransaction();
try {
$model1->save(false);
$model2->updateAll(['status' => 1], ['id' => $id]);
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
批量更新失败时事务不回滚的隐藏坑
使用 updateAll() 或 deleteAll() 时,若 WHERE 条件匹配 0 行,这些方法默认返回 0 而不抛异常,容易被当成“成功”跳过错误检查。实际业务中这常导致逻辑断裂(比如应更新 1 条却更新了 0 条),但事务仍会 commit。
- 务必手动校验影响行数:
if (($affected = Model::updateAll(...)) === 0) { throw new \RuntimeException('No rows updated'); } - 避免在事务中混用 DAO 查询(
createCommand())和 ActiveRecord 操作,除非确认两者共享同一连接(可通过Yii::$app->db->getActiveConnection()对比getMasterPdo()验证) - MySQL 默认隔离级别是
REPEATABLE READ,但在长事务中读取未提交数据可能引发幻读;如需强一致性,考虑加SELECT ... FOR UPDATE,但要小心死锁
事务嵌套与 Connection 复用导致回滚失效
Yii2 不支持真正的嵌套事务(即 savepoint),多次 beginTransaction() 只会返回同一个事务对象,rollBack() 会回滚最外层。更危险的是:如果某处代码(如行为、事件监听器、日志组件)偷偷调用了另一个 DB 操作,它可能走的是新连接,完全游离于当前事务之外。
- 禁用自动重连:在 DB 配置中设
'enableSchemaCache' => true和'schemaCache' => 'cache',但不要设'autoConnect' => false,否则事务 begin 会失败 - 调试时可在事务前后打印
Yii::$app->db->getActiveConnection()->pdo->getAttribute(\PDO::ATTR_CONNECTION_STATUS),确认是否始终为 active - 单元测试里尤其注意:每个 test 方法应独立 setup/teardown DB 连接,否则上一个 test 的未关闭事务会影响下一个
$transaction 对象。最容易被忽略的,是第三方扩展或自定义行为里静默触发的 DB 写操作。


















