在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()?進行回滾。
## 創建事務[](http://www.digpage.com/transaction.html#id2 "Permalink to this headline")
在上面代碼中,我們使用數據庫連接的?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?配置項的值。
## 啟用事務[](http://www.digpage.com/transaction.html#id3 "Permalink to this headline")
上面的代碼告訴我們,啟用事務,最終是靠調用?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?來實現設置事務保存點和回滾到保存點的操作。
## 嵌套事務[](http://www.digpage.com/transaction.html#id4 "Permalink to this headline")
在開頭的例子中,展現的是事務最簡單的使用形式。Yii還允許把事務嵌套起來使用。 比如,可以采用如下形式來使用事務:
~~~
$outerTransaction = $db->beginTransaction();
try {
$db->createCommand($sql1)->execute();
$innerTransaction = $db->beginTransaction();
try {
$db->createCommand($sql2)->execute();
$db->createCommand($sql3)->execute();
$innerTransaction->commit();
} catch (Exception $e) {
$innerTransaction->rollBack();
}
$db->createCommand($sql4)->execute();
$outerTransaction->commit();
} catch (Exception $e) {
$outerTransaction->rollBack();
}
~~~
為了實現這一嵌套,Yii使用?yii\db\Transaction::_level?來表示嵌套的層級。 當層級為 0 時,表示的是最外層的事務。
一般情況下,整個Yii應用使用了同一個數據庫連接,或者說是使用了單例。 具體可以看?[_服務定位器(Service Locator)_](http://www.digpage.com/service_locator.html#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;
}
// 看看啟用事務時,是如何使用事務對象的
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應用,使用了同一個?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()?。
當然,為了使代碼功能有一定的層次感,在必要時,也可以使用嵌套的事務。 但要考慮好,子事務是否真的要吞掉異常?有沒有必要繼續拋出異常, 使得上一層級的事務也產生回滾?這個要根據實際的情形來確定。
## 提交和回滾[](http://www.digpage.com/transaction.html#id5 "Permalink to this headline")
提交和回滾通過?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
if ($this->_level == 0) {
Yii::trace('Commit transaction', __METHOD__);
$this->db->pdo->commit();
$this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION);
return;
}
// 以下是尚未回到最外層的情形
$schema = $this->db->getSchema();
if ($schema->supportsSavepoint()) {
Yii::trace('Release savepoint ' . $this->_level, __METHOD__);
// 釋放那么保存點
$schema->releaseSavepoint('LEVEL' . $this->_level);
} else {
Yii::info('Transaction not committed: nested transaction not supported', __METHOD__);
}
}
public function rollBack()
{
if (!$this->getIsActive()) {
return;
}
// 調用 rollBack() 也會使 _level 自減1
$this->_level--;
// 如果已經返回到最外層,那么調用 PDO 的 rollBack
if ($this->_level == 0) {
Yii::trace('Roll back transaction', __METHOD__);
$this->db->pdo->rollBack();
$this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION);
return;
}
// 以下是未返回到最外層的情形
$schema = $this->db->getSchema();
if ($schema->supportsSavepoint()) {
Yii::trace('Roll back to savepoint ' . $this->_level, __METHOD__);
// 那么就回滾到保存點
$schema->rollBackSavepoint('LEVEL' . $this->_level);
} else {
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
* 對于嵌套的內層的提交和回滾,使用的其實是事務保存點的釋放和回滾
* 釋放保存點時,會釋放保存點的標識符,這個標識符在下次事務嵌套達到這個層級時, 會被再次使用。
## 有效的事務[](http://www.digpage.com/transaction.html#id6 "Permalink to this headline")
在上面的提交、回滾等方法的代碼中,我們多次看到了一個?this->getIsActive()?。 這是用于判斷當前事務是否有效的一個方法,我們通過它,來看看什么樣的一個事務, 算是有效的:
~~~
public function getIsActive()
{
return $this->_level > 0 && $this->db && $this->db->isActive;
}
~~~
方法很簡單明了,一個有效的事務必須同時滿足3個條件:
* _level?>?0?。這是由于為0是,要么是剛剛初始化, 要么是所有的事務已經提交或回滾了。也就是說,只有調用過了?begin()?但還沒有調用過匹配的?commit()?或?rollBack()?的事務對象,才是有效的。
* 數據庫連接要已經初始化。
* 數據庫連接也必須是有效的。
如果覺得《深入理解Yii2.0》對您有所幫助,也請[幫助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 謝謝!
- 更新記錄
- 導讀
- Yii是什么
- Yii2.0的亮點
- 背景知識
- 如何閱讀本書
- Yii基礎
- 屬性(Property)
- 事件(Event)
- 行為(Behavior)
- Yii約定
- Yii應用的目錄結構和入口腳本
- 別名(Alias)
- Yii的類自動加載機制
- 環境和配置文件
- 配置項(Configuration)
- Yii模式
- MVC
- 依賴注入和依賴注入容器
- 服務定位器(Service Locator)
- 請求與響應(TBD)
- 路由(Route)
- Url管理
- 請求(Reqeust)
- Web應用Request
- Yii與數據庫(TBD)
- 數據類型
- 事務(Transaction)
- AcitveReocrd事件和關聯操作
- 樂觀鎖與悲觀鎖
- 《深入理解Yii2.0》視頻教程
- 第一講:基礎配置
- 第二講:用戶登錄
- 第三講:文章及評論的模型
- 附錄
- 附錄1:Yii2.0 對比 Yii1.1 的重大改進
- 附錄2:Yii的安裝
- 熱心讀者