事務(Transaction)
在Yii中,使用 yii\db\Transaction 來表示資料庫事務。
一般情況下,我們從資料庫連接配接啟用事務,通常采用如下的形式:
$transaction=$connection->beginTransaction();
try {
$connection->createCommand($sql1)->execute();
$connection->createCommand($sql2)->execute();
// ... executing other SQL statements ...
$transaction->commit();
} catch (Exception $e) {
$transaction->rollBack();
}
在上面的代碼中,先是擷取一個 yii\db\Transaction 對象,之後執行若幹SQL 語句,然後調用之前 Transaction 對象的 commit() 方法。這一過程中, 如果捕獲了異常,那麼調用 rollBack() 進行復原。
建立事務
在上面代碼中,我們使用資料庫連接配接的 beginTransaction() 方法, 建立了一個 yii\db\Trnasaction 對象,具體代碼在 yii\db\Connection 中:
public function beginTransaction($isolationLevel=null)
{
$this->open();
// 尚未初始化目前連接配接使用的Transaction對象,則建立一個
if (($transaction=$this->getTransaction())===null) {
$transaction=$this->_transaction=new Transaction(['db'=> $this]);
// 擷取Transaction後,就可以啟用事務
$transaction->begin($isolationLevel);
return $transaction;
從建立 Transaction 對象的 new Transaction(['db'=> $this]) 形式來看, 這也是Yii一貫的風格。這裡簡單的初始化了 yii\db\Transaction::db 。
這表示的是目前的 Transaction 所依賴的資料庫連接配接。如果未對其進行初始化, 那麼将無法正常使用事務。
在擷取了 Transaction 之後,就可以調用他的 begin() 方法,來啟用事務。 必要的情況下,還可以指定事務隔離級别。
事務隔離級别的設定,由 yii\db\
Schema::setTransactionIsolationLevel() 方法來實作,而這個方法,無非就是執行了如下的SQL語句:
SET TRANSACTION ISOLATION LEVEL ...
對于隔離級别,yii\db\Transaction 也提前定義了幾個常量:
const READ_UNCOMMITTED='READ UNCOMMITTED';
const READ_COMMITTED='READ COMMITTED';
const REPEATABLE_READ='REPEATABLE READ';
const SERIALIZABLE='SERIALIZABLE';
如果開發者沒有給出隔離級别,那麼,資料庫會使用預設配置的隔離級别。 比如,對于MySQL而言,就是使用 transaction-isolation 配置項的值。
啟用事務
上面的代碼告訴我們,啟用事務,最終是靠調用 Transaction::begin() 來實作的。 那麼就讓我們來看看他的代碼吧:
public function begin($isolationLevel=null)
// 沒有初始化資料庫連接配接的滾粗
if ($this->db===null) {
throw new InvalidConfigException('Transaction::db must be set.');
$this->db->open();
// _level 為0 表示的是最外層的事務
if ($this->_level==0) {
// 如果給定了隔離級别,那麼就設定之
if ($isolationLevel !==null) {
// 設定事務隔離級别
$this->db->getSchema()->setTransactionIsolationLevel($isolationLevel);
Yii::trace('Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : ''), __METHOD__);
$this->db->trigger(Connection::EVENT_BEGIN_TRANSACTION);
$this->db->pdo->beginTransaction();
$this->_level=1;
return;
// 以下 _level>0 表示的是嵌套的事務
$schema=$this->db->getSchema();
// 要使用嵌套事務,前提是所使用的資料庫要支援
if ($schema->supportsSavepoint()) {
Yii::trace('Set savepoint ' . $this->_level, __METHOD__);
// 使用事務儲存點
$schema->createSavepoint('LEVEL' . $this->_level);
} else {
Yii::info('Transaction not started: nested transaction not supported', __METHOD__);
// 結合 _level==0 分支中的 $this->_level=1,
// 可以得知,一旦調用這個方法, _level 就會自增1
$this->_level++;
對于最外層的事務,即當 _level 為 0 時,最終落到PDO的 beginTransaction() 來啟用事務。在啟用前,如果開發者給定了隔離級别,那麼還需要設定隔離級别。
當 _level > 0 時,表示的是嵌套的事務,并非最外層的事務。 對此,Yii使用 SQL 的 SAVEPOINT 和 ROLLBACK TO SAVEPOINT 來實作設定事務儲存點和復原到儲存點的操作。
嵌套事務
在開頭的例子中,展現的是事務最簡單的使用形式。Yii還允許把事務嵌套起來使用。 比如,可以采用如下形式來使用事務:
18
$outerTransaction=$db->beginTransaction();
$db->createCommand($sql1)->execute();
$innerTransaction=$db->beginTransaction();
$db->createCommand($sql2)->execute();
$db->createCommand($sql3)->execute();
$innerTransaction->commit();
$innerTransaction->rollBack();
$db->createCommand($sql4)->execute();
$outerTransaction->commit();
$outerTransaction->rollBack();
為了實作這一嵌套,Yii使用 yii\db\Transaction::_level 來表示嵌套的層級。 當層級為 0 時,表示的是最外層的事務。
一般情況下,整個Yii應用使用了同一個資料庫連接配接,或者說是使用了單例。 具體可以看 服務定位器(Service Locator) 部分。
而在 yii\db\Connection 中,又對事務對象進行了緩存:
class Connection extends Component
// 儲存目前連接配接的有效Transaction對象
private $_transaction;
// 已經緩存有事務對象,且事務對象有效,則傳回該事務對象
// 否則傳回null
public function getTransaction()
return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null;
// 看看啟用事務時,是如何使用
二手手機靓号轉讓平台事務對象的
// 緩存的事務對象有效,則使用緩存中的事務對象
// 否則建立一個新的事務對象
是以,可以認為整個Yii應用,使用了同一個 Transaction 對象,也就是說, Transaction::_level 在整個應用的生命周期中,是有延續性的。 這是實作事務嵌套的關鍵和前提。
在這個 Transaction::_level 的基礎上,Yii實作了事務的嵌套:
事務對象初始化時,設 _level 為0,表示如果要啟用事務, 這是一個最外層的事務。
每當調用 Transaction::begin() 來啟用具體事務時, _level 自增1。 表示如再啟用事務,将是層級為1的嵌套事務。
每當調用 Transaction::commit() 或 Transaction::rollBack() 時, _level 自減1,表示目前層級的事務處理完畢,傳回上一層級的事務中。
當調用了一次 begin() 且還沒有調用比對的 commit() 或 rollBack() , 就再次調用 begin() 時,會使事務進行更深一層級的嵌套中。
是以,就有了我們上面代碼中,當 _level 為 0 時,需要設定事務隔離級别。 因為這是最外層事務。
而當 _level > 0 時,由于是“嵌套”的事務,一個大事務中的小“事務”,那麼, 就使用儲存點及其復原、釋放操作,來模拟事務的啟用、復原和送出操作。
要注意,在這一節的開頭,我們使用2對嵌套的 try ... catch 來實作事務的嵌套。 由于内層的 catch 把可能抛出的異常吞了,不再繼續抛出。那麼, 外層的 catch ,是捕獲不到内層的異常的。
也就是說,這種情況下,外層中的 $sql1 $sql4 不會由于 $sql2 或 $sql3 的失敗而中止, $sql1 $sql4 可以繼續執行并 commit 。
這是嵌套事務的正确使用形式,即内外層之間應當是不相幹的。
如果内層事務的異常,會導緻外層事務需要復原,那麼我們不應該使用事務嵌套, 而是應該把内外層當成一個事務。這個道理很淺顯,但是事實開發中,一個不小心, 就會出昏招。是以,不要動不動就來個 beginTransaction() 。
當然,為了使代碼功能有一定的層次感,在必要時,也可以使用嵌套的事務。 但要考慮好,子事務是否真的要吞掉異常?有沒有必要繼續抛出異常, 使得上一層級的事務也産生復原?這個要根據實際的情形來确定。
送出和復原
送出和復原通過 Transaction::commit() 和 Transaction::rollBack() 來實作:
public function commit()
if (!$this->getIsActive()) {
throw new Exception('Failed to commit transaction: transaction was inactive.');
// 與begin()對應,隻要調用 commit(),_level 自減1
$this->_level--;
// 如果回到了最外層事務,那麼應當使用PDO的commit
Yii::trace('Commit transaction', __METHOD__);
$this->db->pdo->commit();
$this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION);
// 以下是尚未回到最外層的情形
Yii::trace('Release savepoint ' . $this->_level, __METHOD__);
// 釋放那麼儲存點
$schema->releaseSavepoint('LEVEL' . $this->_level);
Yii::info('Transaction not committed: nested transaction not supported', __METHOD__);
public function rollBack()
// 調用 rollBack() 也會使 _level 自減1
// 如果已經傳回到最外層,那麼調用 PDO 的 rollBack
Yii::trace('Roll back transaction', __METHOD__);
$this->db->pdo->rollBack();
$this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION);
// 以下是未傳回到最外層的情形
Yii::trace('Roll back to savepoint ' . $this->_level, __METHOD__);
// 那麼就復原到儲存點
$schema->rollBackSavepoint('LEVEL' . $this->_level);
Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__);
throw new Exception('Roll back failed: nested transaction not supported.');
對于送出和復原:
送出時,會使層級+1,復原時,會使層級-1
對于最外層的送出和復原,使用的是資料庫事務的 commit 和 rollBack
對于嵌套的内層的送出和復原,使用的其實是事務儲存點的釋放和復原
釋放儲存點時,會釋放儲存點的辨別符,這個辨別符在下次事務嵌套達到這個層級時, 會被再次使用。
有效的事務
在上面的送出、復原等方法的代碼中,我們多次看到了一個 this->getIsActive() 。 這是用于判斷目前事務是否有效的一個方法,我們通過它,來看看什麼樣的一個事務, 算是有效的:
public function getIsActive()
return $this->_level > 0 && $this->db && $this->db->isActive;
方法很簡單明了,一個有效的事務必須同時滿足3個條件:
_level > 0 。這是由于為0是,要麼是剛剛初始化, 要麼是所有的事務已經送出或復原了。也就是說,隻有調用過了 begin() 但還沒有調用過比對的 commit() 或 rollBack() 的事務對象,才是有效的。
資料庫連接配接要已經初始化。
資料庫連接配接也必須是有效的。