業務邏輯往往涉及多張表的寫入操作(如:用戶註冊需同時寫入 user、profile、balance 表)。若中間某步失敗,已寫入的數據必須回滾,保證數據一致性。Yii2 基於 PDO 封裝了 yii\db\Transaction,支援嵌套事務(保存點)與多模型協同。
| 方法 | 說明 |
|---|---|
Yii::$app->db->beginTransaction() | 開啟事務,返回 Transaction 實例 |
$tr->commit() | 提交事務 |
$tr->rollBack() | 回滾事務 |
$tr->getIsActive() | 判斷事務是否處於活動狀態 |
public function actionTestException() {
$tr = Yii::$app->db->beginTransaction();
try {
for ($i=1; $i<=3; $i++) {
$area = new Areas();
$area->name = 'name'.$i;
$area->sort = 1;
$area->save();
}
// 故意觸發異常:字段名拼寫錯誤
$area = new Areas();
$area->name = 'ab'.$i;
$area->sorta = 1; // 不存在的屬性
$area->save(); // 拋出 Exception
$tr->commit();
} catch (\Exception $e) {
$tr->rollBack();
echo 'rollback: ' . $e->getMessage();
}
}
// 結果:三條數據均未入庫,事務完美回滾
ActiveRecord::save() 失敗時返回 false 而非拋出異常,必須手動 throw new \yii\db\Exception() 觸發回滾。
public function actionTestLogicFail() {
$tr = Yii::$app->db->beginTransaction();
try {
for ($i=1; $i<=3; $i++) {
$area = new Areas();
$area->name = 'name'.$i;
$area->sort = 1;
if (!$area->save()) throw new \yii\db\Exception('保存失敗');
}
$area = new Areas();
$area->name = null; // 假設 name 非空約束
$area->sort = 1;
if (!$area->save()) throw new \yii\db\Exception('關鍵業務失敗');
$tr->commit();
} catch (\Exception $e) {
$tr->rollBack();
echo 'rollback: ' . $e->getMessage();
}
}
// 結果:前三條也被回滾,保證原子性
真實項目中,寫入邏輯分散在各 Model 靜態方法中,Controller 只負責編排。關鍵在於全庫共用同一 DB 連接(單例),Yii2 Connection::beginTransaction() 內部緩存了 Transaction 實例,同一請求周期內多次調用返回同一對象,實現天然的「隱式傳遞」。
// Controller
public function actionReg() {
$tr = Yii::$app->db->beginTransaction();
try {
$uid = App::getSeNo(); // 發號器(寫 sequence 表)
UserProfile::add($uid, $name, $gender, $sign); // 寫用戶資料
UserBalance::initUserBalance($uid); // 初始化余額
$tr->commit();
return ['success'=>true, 'balance'=>UserBalance::findOne(['user_id'=>$uid])];
} catch (\Exception $e) {
$tr->rollBack();
return ['success'=>false, 'msg'=>$e->getMessage()];
}
}
// UserProfile::add()
public static function add($uid, $name, $gender, $sign) {
$model = new self();
$model->user_id = $uid;
$model->nikename = $name;
$model->gender = $gender;
$model->intro = $sign;
if (!$model->insert()) throw new \yii\db\Exception('用戶資料寫入失敗');
return $model;
}
// UserBalance::initUserBalance()
public static function initUserBalance($uid) {
$model = new self();
$model->user_id = $uid;
$model->price = '0';
if (!$model->insert()) throw new \yii\db\Exception('余額初始化失敗');
return $model;
}
// yii\db\Connection 簡化源碼
private $_transaction;
public function getTransaction() {
return $this->_transaction && $this->_transaction->getIsActive()
? $this->_transaction : null;
}
public function beginTransaction($isolationLevel = null) {
$this->open();
if (($transaction = $this->getTransaction()) === null) {
$transaction = $this->_transaction = new Transaction(['db'=>$this]);
}
$transaction->begin($isolationLevel);
return $transaction;
}
同一請求內,Yii::$app->db 是單例,_transaction 緩存確保所有 beginTransaction() 返回同一 Transaction 實例,_level 計數器實現嵌套(保存點)支持。
save()/insert()/update() 失敗必須 throw new \yii\db\Exception()。catch (\Throwable $e)(PHP 7+)兼容 Error 與 Exception。