Commit bea9e3fc by Qiang Xue

Fixes #1645: Added support for nested DB transactions

parent f6c0b4c2
...@@ -210,7 +210,7 @@ $command->execute(); ...@@ -210,7 +210,7 @@ $command->execute();
Transactions Transactions
------------ ------------
If the underlying DBMS supports transactions, you can perform transactional SQL queries like the following: You can perform transactional SQL queries like the following:
```php ```php
$transaction = $connection->beginTransaction(); $transaction = $connection->beginTransaction();
...@@ -220,10 +220,34 @@ try { ...@@ -220,10 +220,34 @@ try {
// ... executing other SQL statements ... // ... executing other SQL statements ...
$transaction->commit(); $transaction->commit();
} catch(Exception $e) { } catch(Exception $e) {
$transaction->rollback(); $transaction->rollBack();
} }
``` ```
You can also nest multiple transactions, if needed:
```php
// outer transaction
$transaction1 = $connection->beginTransaction();
try {
$connection->createCommand($sql1)->execute();
// inner transaction
$transaction2 = $connection->beginTransaction();
try {
$connection->createCommand($sql2)->execute();
$transaction2->commit();
} catch (Exception $e) {
$transaction2->rollBack();
}
$transaction1->commit();
} catch (Exception $e) {
$transaction1->rollBack();
}
```
Working with database schema Working with database schema
---------------------------- ----------------------------
......
...@@ -372,12 +372,12 @@ abstract class ActiveRecord extends BaseActiveRecord ...@@ -372,12 +372,12 @@ abstract class ActiveRecord extends BaseActiveRecord
try { try {
$result = $this->insertInternal($attributes); $result = $this->insertInternal($attributes);
if ($result === false) { if ($result === false) {
$transaction->rollback(); $transaction->rollBack();
} else { } else {
$transaction->commit(); $transaction->commit();
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$transaction->rollback(); $transaction->rollBack();
throw $e; throw $e;
} }
} else { } else {
...@@ -473,12 +473,12 @@ abstract class ActiveRecord extends BaseActiveRecord ...@@ -473,12 +473,12 @@ abstract class ActiveRecord extends BaseActiveRecord
try { try {
$result = $this->updateInternal($attributes); $result = $this->updateInternal($attributes);
if ($result === false) { if ($result === false) {
$transaction->rollback(); $transaction->rollBack();
} else { } else {
$transaction->commit(); $transaction->commit();
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$transaction->rollback(); $transaction->rollBack();
throw $e; throw $e;
} }
} else { } else {
...@@ -589,14 +589,14 @@ abstract class ActiveRecord extends BaseActiveRecord ...@@ -589,14 +589,14 @@ abstract class ActiveRecord extends BaseActiveRecord
} }
if ($transaction !== null) { if ($transaction !== null) {
if ($result === false) { if ($result === false) {
$transaction->rollback(); $transaction->rollBack();
} else { } else {
$transaction->commit(); $transaction->commit();
} }
} }
} catch (\Exception $e) { } catch (\Exception $e) {
if ($transaction !== null) { if ($transaction !== null) {
$transaction->rollback(); $transaction->rollBack();
} }
throw $e; throw $e;
} }
......
...@@ -79,6 +79,7 @@ Yii Framework 2 Change Log ...@@ -79,6 +79,7 @@ Yii Framework 2 Change Log
- Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue) - Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue)
- Enh #1646: Added postgresql `QueryBuilder::checkIntegrity` and `QueryBuilder::resetSequence` (Ragazzo) - Enh #1646: Added postgresql `QueryBuilder::checkIntegrity` and `QueryBuilder::resetSequence` (Ragazzo)
- Enh #1645: Added `Connection::$pdoClass` property (Ragazzo) - Enh #1645: Added `Connection::$pdoClass` property (Ragazzo)
- Enh #1645: Added support for nested DB transactions (qiangxue)
- Enh #1681: Added support for automatically adjusting the "for" attribute of label generated by `ActiveField::label()` (qiangxue) - Enh #1681: Added support for automatically adjusting the "for" attribute of label generated by `ActiveField::label()` (qiangxue)
- Enh #1706: Added support for registering a single JS/CSS file with dependency (qiangxue) - Enh #1706: Added support for registering a single JS/CSS file with dependency (qiangxue)
- Enh #1773: keyPrefix property of Cache is not restricted to alnum characters anymore, however it is still recommended (cebe) - Enh #1773: keyPrefix property of Cache is not restricted to alnum characters anymore, however it is still recommended (cebe)
......
...@@ -337,12 +337,12 @@ class ActiveRecord extends BaseActiveRecord ...@@ -337,12 +337,12 @@ class ActiveRecord extends BaseActiveRecord
try { try {
$result = $this->insertInternal($attributes); $result = $this->insertInternal($attributes);
if ($result === false) { if ($result === false) {
$transaction->rollback(); $transaction->rollBack();
} else { } else {
$transaction->commit(); $transaction->commit();
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$transaction->rollback(); $transaction->rollBack();
throw $e; throw $e;
} }
} else { } else {
...@@ -449,12 +449,12 @@ class ActiveRecord extends BaseActiveRecord ...@@ -449,12 +449,12 @@ class ActiveRecord extends BaseActiveRecord
try { try {
$result = $this->updateInternal($attributes); $result = $this->updateInternal($attributes);
if ($result === false) { if ($result === false) {
$transaction->rollback(); $transaction->rollBack();
} else { } else {
$transaction->commit(); $transaction->commit();
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$transaction->rollback(); $transaction->rollBack();
throw $e; throw $e;
} }
} else { } else {
...@@ -505,14 +505,14 @@ class ActiveRecord extends BaseActiveRecord ...@@ -505,14 +505,14 @@ class ActiveRecord extends BaseActiveRecord
} }
if ($transaction !== null) { if ($transaction !== null) {
if ($result === false) { if ($result === false) {
$transaction->rollback(); $transaction->rollBack();
} else { } else {
$transaction->commit(); $transaction->commit();
} }
} }
} catch (\Exception $e) { } catch (\Exception $e) {
if ($transaction !== null) { if ($transaction !== null) {
$transaction->rollback(); $transaction->rollBack();
} }
throw $e; throw $e;
} }
......
...@@ -68,7 +68,7 @@ use yii\caching\Cache; ...@@ -68,7 +68,7 @@ use yii\caching\Cache;
* // ... executing other SQL statements ... * // ... executing other SQL statements ...
* $transaction->commit(); * $transaction->commit();
* } catch(Exception $e) { * } catch(Exception $e) {
* $transaction->rollback(); * $transaction->rollBack();
* } * }
* ~~~ * ~~~
* *
...@@ -396,7 +396,7 @@ class Connection extends Component ...@@ -396,7 +396,7 @@ class Connection extends Component
*/ */
public function getTransaction() public function getTransaction()
{ {
return $this->_transaction && $this->_transaction->isActive ? $this->_transaction : null; return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null;
} }
/** /**
...@@ -406,9 +406,12 @@ class Connection extends Component ...@@ -406,9 +406,12 @@ class Connection extends Component
public function beginTransaction() public function beginTransaction()
{ {
$this->open(); $this->open();
$this->_transaction = new Transaction(['db' => $this]);
$this->_transaction->begin(); if (($transaction = $this->getTransaction()) === null) {
return $this->_transaction; $transaction = $this->_transaction = new Transaction(['db' => $this]);
}
$transaction->begin();
return $transaction;
} }
/** /**
......
...@@ -64,14 +64,14 @@ class Migration extends \yii\base\Component ...@@ -64,14 +64,14 @@ class Migration extends \yii\base\Component
$transaction = $this->db->beginTransaction(); $transaction = $this->db->beginTransaction();
try { try {
if ($this->safeUp() === false) { if ($this->safeUp() === false) {
$transaction->rollback(); $transaction->rollBack();
return false; return false;
} }
$transaction->commit(); $transaction->commit();
} catch (\Exception $e) { } catch (\Exception $e) {
echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n";
echo $e->getTraceAsString() . "\n"; echo $e->getTraceAsString() . "\n";
$transaction->rollback(); $transaction->rollBack();
return false; return false;
} }
return null; return null;
...@@ -89,14 +89,14 @@ class Migration extends \yii\base\Component ...@@ -89,14 +89,14 @@ class Migration extends \yii\base\Component
$transaction = $this->db->beginTransaction(); $transaction = $this->db->beginTransaction();
try { try {
if ($this->safeDown() === false) { if ($this->safeDown() === false) {
$transaction->rollback(); $transaction->rollBack();
return false; return false;
} }
$transaction->commit(); $transaction->commit();
} catch (\Exception $e) { } catch (\Exception $e) {
echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n";
echo $e->getTraceAsString() . "\n"; echo $e->getTraceAsString() . "\n";
$transaction->rollback(); $transaction->rollBack();
return false; return false;
} }
return null; return null;
......
...@@ -290,6 +290,41 @@ abstract class Schema extends Object ...@@ -290,6 +290,41 @@ abstract class Schema extends Object
} }
/** /**
* @return boolean whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
*/
public function supportsSavepoint()
{
return true;
}
/**
* Creates a new savepoint.
* @param string $name the savepoint name
*/
public function createSavepoint($name)
{
$this->db->createCommand("SAVEPOINT $name")->execute();
}
/**
* Releases an existing savepoint.
* @param string $name the savepoint name
*/
public function releaseSavepoint($name)
{
$this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
}
/**
* Rolls back to a previously created savepoint.
* @param string $name the savepoint name
*/
public function rollBackSavepoint($name)
{
$this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
}
/**
* Quotes a string value for use in a query. * Quotes a string value for use in a query.
* Note that if the parameter is not a string, it will be returned without change. * Note that if the parameter is not a string, it will be returned without change.
* @param string $str string to be quoted * @param string $str string to be quoted
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace yii\db; namespace yii\db;
use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
/** /**
...@@ -25,12 +26,12 @@ use yii\base\InvalidConfigException; ...@@ -25,12 +26,12 @@ use yii\base\InvalidConfigException;
* //.... other SQL executions * //.... other SQL executions
* $transaction->commit(); * $transaction->commit();
* } catch(Exception $e) { * } catch(Exception $e) {
* $transaction->rollback(); * $transaction->rollBack();
* } * }
* ~~~ * ~~~
* *
* @property boolean $isActive Whether this transaction is active. Only an active transaction can [[commit()]] * @property boolean $isActive Whether this transaction is active. Only an active transaction can [[commit()]]
* or [[rollback()]]. This property is read-only. * or [[rollBack()]]. This property is read-only.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
...@@ -42,19 +43,18 @@ class Transaction extends \yii\base\Object ...@@ -42,19 +43,18 @@ class Transaction extends \yii\base\Object
*/ */
public $db; public $db;
/** /**
* @var boolean whether this transaction is active. Only an active transaction * @var integer the nesting level of the transaction. 0 means the outermost level.
* can [[commit()]] or [[rollback()]]. This property is set true when the transaction is started.
*/ */
private $_active = false; private $_level = 0;
/** /**
* Returns a value indicating whether this transaction is active. * Returns a value indicating whether this transaction is active.
* @return boolean whether this transaction is active. Only an active transaction * @return boolean whether this transaction is active. Only an active transaction
* can [[commit()]] or [[rollback()]]. * can [[commit()]] or [[rollBack()]].
*/ */
public function getIsActive() public function getIsActive()
{ {
return $this->_active; return $this->_level > 0 && $this->db && $this->db->isActive;
} }
/** /**
...@@ -63,44 +63,79 @@ class Transaction extends \yii\base\Object ...@@ -63,44 +63,79 @@ class Transaction extends \yii\base\Object
*/ */
public function begin() public function begin()
{ {
if (!$this->_active) {
if ($this->db === null) { if ($this->db === null) {
throw new InvalidConfigException('Transaction::db must be set.'); throw new InvalidConfigException('Transaction::db must be set.');
} }
\Yii::trace('Starting transaction', __METHOD__);
$this->db->open(); $this->db->open();
if ($this->_level == 0) {
Yii::trace('Begin transaction', __METHOD__);
$this->db->pdo->beginTransaction(); $this->db->pdo->beginTransaction();
$this->_active = true; $this->_level = 1;
return;
}
$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__);
} }
$this->_level++;
} }
/** /**
* Commits a transaction. * Commits a transaction.
* @throws Exception if the transaction or the [[db|DB connection]] is not active. * @throws Exception if the transaction is not active
*/ */
public function commit() public function commit()
{ {
if ($this->_active && $this->db && $this->db->isActive) { if (!$this->getIsActive()) {
\Yii::trace('Committing transaction', __METHOD__); throw new Exception('Failed to commit transaction: transaction was inactive.');
}
$this->_level--;
if ($this->_level == 0) {
Yii::trace('Commit transaction', __METHOD__);
$this->db->pdo->commit(); $this->db->pdo->commit();
$this->_active = false; return;
}
$schema = $this->db->getSchema();
if ($schema->supportsSavepoint()) {
Yii::trace('Release savepoint ' . $this->_level, __METHOD__);
$schema->releaseSavepoint('LEVEL' . $this->_level);
} else { } else {
throw new Exception('Failed to commit transaction: transaction was inactive.'); Yii::info('Transaction not committed: nested transaction not supported', __METHOD__);
} }
} }
/** /**
* Rolls back a transaction. * Rolls back a transaction.
* @throws Exception if the transaction or the [[db|DB connection]] is not active. * @throws Exception if the transaction is not active
*/ */
public function rollback() public function rollBack()
{ {
if ($this->_active && $this->db && $this->db->isActive) { if (!$this->getIsActive()) {
\Yii::trace('Rolling back transaction', __METHOD__); throw new Exception('Failed to roll back transaction: transaction was inactive.');
}
$this->_level--;
if ($this->_level == 0) {
Yii::trace('Roll back transaction', __METHOD__);
$this->db->pdo->rollBack(); $this->db->pdo->rollBack();
$this->_active = false; return;
}
$schema = $this->db->getSchema();
if ($schema->supportsSavepoint()) {
Yii::trace('Roll back to savepoint ' . $this->_level, __METHOD__);
$schema->rollBackSavepoint('LEVEL' . $this->_level);
} else { } else {
throw new Exception('Failed to roll back transaction: transaction was inactive.'); Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__);
// throw an exception to fail the outer transaction
throw new Exception('Roll back failed: nested transaction not supported.');
} }
} }
} }
...@@ -64,6 +64,15 @@ class Schema extends \yii\db\Schema ...@@ -64,6 +64,15 @@ class Schema extends \yii\db\Schema
'enum' => self::TYPE_STRING, 'enum' => self::TYPE_STRING,
]; ];
/**
* @inheritdoc
*/
public function releaseSavepoint($name)
{
// does nothing as cubrid does not support this
}
/** /**
* Quotes a table name for use in a query. * Quotes a table name for use in a query.
* A simple table name has no schema prefix. * A simple table name has no schema prefix.
......
...@@ -51,7 +51,7 @@ class PDO extends \PDO ...@@ -51,7 +51,7 @@ class PDO extends \PDO
/** /**
* Rollbacks a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not * Rollbacks a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not
* natively support transactions. * natively support transactions.
* @return boolean the result of a transaction rollback. * @return boolean the result of a transaction roll back.
*/ */
public function rollBack() public function rollBack()
{ {
......
...@@ -74,6 +74,30 @@ class Schema extends \yii\db\Schema ...@@ -74,6 +74,30 @@ class Schema extends \yii\db\Schema
]; ];
/** /**
* @inheritdoc
*/
public function createSavepoint($name)
{
$this->db->createCommand("SAVE TRANSACTION $name")->execute();
}
/**
* @inheritdoc
*/
public function releaseSavepoint($name)
{
// does nothing as MSSQL does not support this
}
/**
* @inheritdoc
*/
public function rollBackSavepoint($name)
{
$this->db->createCommand("ROLLBACK TRANSACTION $name")->execute();
}
/**
* Quotes a table name for use in a query. * Quotes a table name for use in a query.
* A simple table name has no schema prefix. * A simple table name has no schema prefix.
* @param string $name table name. * @param string $name table name.
......
...@@ -34,6 +34,14 @@ class Schema extends \yii\db\Schema ...@@ -34,6 +34,14 @@ class Schema extends \yii\db\Schema
/** /**
* @inheritdoc * @inheritdoc
*/ */
public function releaseSavepoint($name)
{
// does nothing as Oracle does not support this
}
/**
* @inheritdoc
*/
public function quoteSimpleTableName($name) public function quoteSimpleTableName($name)
{ {
return '"' . $name . '"'; return '"' . $name . '"';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment