事务必须显式开启,不能依赖AR自动包裹;需通过Yii::$app->db->beginTransaction()手动启动,并在try-catch中统一commit/rollback,确保原子性。

事务必须显式开启,不能靠 AR 自动包裹
Yii 的 ActiveRecord 操作默认不开启事务,哪怕你连续调用 save() 或 delete() 多次,每条语句都是独立提交的。一旦中间出错,前面成功的操作不会回滚——这在转账、订单创建+库存扣减等场景下是致命的。
开启事务的唯一可靠方式是手动获取 DB 连接并调用 beginTransaction()。AR 模型本身不持有事务上下文,所有模型操作都依赖当前连接的事务状态。
-
$db = \Yii::$app->db;是事务起点,不是User::getDb()或其他模型方法 - 事务对象(
$transaction)必须保存为变量,后续commit()或rollback()都要靠它 - 不能在事务块外捕获异常后调用
$transaction->rollback(),必须在try...catch内完成
正确写法:try-catch 包裹 + 显式 commit/rollback
下面是最小可运行的事务模板,适用于 Yii2:
$db = \Yii::$app->db;
$transaction = $db->beginTransaction();
try {
$user = new \app\models\User();
$user->name = 'test';
$user->save(); // INSERT
$profile = new \app\models\Profile();
$profile->user_id = $user->id;
$profile->bio = 'hello';
$profile->save(); // INSERT
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollback();
throw $e; // 重新抛出,避免静默失败
}
注意:$user->save() 和 $profile->save() 虽然是 AR 方法,但它们会自动使用当前连接上激活的事务,无需额外传参。
- 如果其中任意一步失败(比如字段验证不通过、数据库约束冲突),
catch会捕获异常并触发rollback() -
throw $e很关键——否则上层控制器可能误以为操作成功 - 不要用
if (!$user->save()) { $transaction->rollback(); }这类逻辑,AR 的save()失败只返回false,但数据库级错误(如唯一键冲突)仍会抛异常
DAO 操作也走同一事务,但要注意参数绑定
如果你混合使用 AR 和原生 SQL(比如用 createCommand() 更新统计表),只要共用同一个 $db 实例,它们就天然处于同一事务中:
$transaction = \Yii::$app->db->beginTransaction();
try {
$user = \app\models\User::findOne(1);
$user->status = 2;
$user->save();
\Yii::$app->db->createCommand()
->update('user_log', ['updated_at' => date('Y-m-d H:i:s')], ['user_id' => 1])
->execute();
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollback();
throw $e;
}
这里的关键是:DAO 的 execute() 不会自动开启新连接,它复用 \Yii::$app->db 的当前连接和事务状态。
- 所有
createCommand()必须调用execute()、queryOne()等执行方法,否则只是构造命令,不真正发往数据库 - 如果 DAO 查询用了占位符(如
:id),bindValue()必须在execute()前调用,否则参数无效 - 事务期间执行的任何查询(包括
SELECT)都会看到未提交的变更,这是预期行为,不是 bug
嵌套事务不被支持,多模型操作别拆开写
Yii 的底层 PDO 不支持真正的嵌套事务(savepoint 是模拟的)。如果你在已有事务中再次调用 beginTransaction(),它不会新建一层,而是静默忽略或报错,取决于 PDO 驱动配置。
常见错误是把事务逻辑分散到不同模型方法里,例如:
// ❌ 错误:User::createWithProfile() 内部自己开了事务 $user = \app\models\User::createWithProfile($data); // 它内部 commit 了 $notification->send(); // 这步失败,前面 user 已经提交,无法回滚
这种写法破坏原子性。正确做法是把所有相关操作收束到一个顶层事务块中,由控制器或服务类统一控制生命周期。
- 模型方法应只负责单步数据操作,不管理事务
- 事务边界要足够宽,覆盖所有强依赖的操作(增删改查都算)
- 长事务(>5 秒)容易引发锁等待或超时,尽量减少非 DB 操作(如 HTTP 请求、文件读写)放在事务内
事务最易被忽略的一点:**没检查 save() 返回值就直接往下走**。AR 的 save() 在验证失败时返回 false,但不会抛异常,此时事务还在进行中,后续操作若成功提交,就会造成数据不一致。务必在 try 块内加 if (!$model->save()) { throw new \Exception('Validation failed'); } 这类兜底判断。











