天天看點

深入了解Yii2 資料庫事務

  事務(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() 的事務對象,才是有效的。

  資料庫連接配接要已經初始化。

  資料庫連接配接也必須是有效的。