Commit bea9e3fc by Qiang Xue

Fixes #1645: Added support for nested DB transactions

parent f6c0b4c2
......@@ -210,7 +210,7 @@ $command->execute();
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
$transaction = $connection->beginTransaction();
......@@ -220,10 +220,34 @@ try {
// ... executing other SQL statements ...
$transaction->commit();
} 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
----------------------------
......
......@@ -372,12 +372,12 @@ abstract class ActiveRecord extends BaseActiveRecord
try {
$result = $this->insertInternal($attributes);
if ($result === false) {
$transaction->rollback();
$transaction->rollBack();
} else {
$transaction->commit();
}
} catch (\Exception $e) {
$transaction->rollback();
$transaction->rollBack();
throw $e;
}
} else {
......@@ -473,12 +473,12 @@ abstract class ActiveRecord extends BaseActiveRecord
try {
$result = $this->updateInternal($attributes);
if ($result === false) {
$transaction->rollback();
$transaction->rollBack();
} else {
$transaction->commit();
}
} catch (\Exception $e) {
$transaction->rollback();
$transaction->rollBack();
throw $e;
}
} else {
......@@ -589,14 +589,14 @@ abstract class ActiveRecord extends BaseActiveRecord
}
if ($transaction !== null) {
if ($result === false) {
$transaction->rollback();
$transaction->rollBack();
} else {
$transaction->commit();
}
}
} catch (\Exception $e) {
if ($transaction !== null) {
$transaction->rollback();
$transaction->rollBack();
}
throw $e;
}
......
......@@ -79,6 +79,7 @@ Yii Framework 2 Change Log
- Enh #1641: Added `BaseActiveRecord::updateAttributes()` (qiangxue)
- Enh #1646: Added postgresql `QueryBuilder::checkIntegrity` and `QueryBuilder::resetSequence` (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 #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)
......
......@@ -337,12 +337,12 @@ class ActiveRecord extends BaseActiveRecord
try {
$result = $this->insertInternal($attributes);
if ($result === false) {
$transaction->rollback();
$transaction->rollBack();
} else {
$transaction->commit();
}
} catch (\Exception $e) {
$transaction->rollback();
$transaction->rollBack();
throw $e;
}
} else {
......@@ -449,12 +449,12 @@ class ActiveRecord extends BaseActiveRecord
try {
$result = $this->updateInternal($attributes);
if ($result === false) {
$transaction->rollback();
$transaction->rollBack();
} else {
$transaction->commit();
}
} catch (\Exception $e) {
$transaction->rollback();
$transaction->rollBack();
throw $e;
}
} else {
......@@ -505,14 +505,14 @@ class ActiveRecord extends BaseActiveRecord
}
if ($transaction !== null) {
if ($result === false) {
$transaction->rollback();
$transaction->rollBack();
} else {
$transaction->commit();
}
}
} catch (\Exception $e) {
if ($transaction !== null) {
$transaction->rollback();
$transaction->rollBack();
}
throw $e;
}
......
......@@ -68,7 +68,7 @@ use yii\caching\Cache;
* // ... executing other SQL statements ...
* $transaction->commit();
* } catch(Exception $e) {
* $transaction->rollback();
* $transaction->rollBack();
* }
* ~~~
*
......@@ -396,7 +396,7 @@ class Connection extends Component
*/
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
public function beginTransaction()
{
$this->open();
$this->_transaction = new Transaction(['db' => $this]);
$this->_transaction->begin();
return $this->_transaction;
if (($transaction = $this->getTransaction()) === null) {
$transaction = $this->_transaction = new Transaction(['db' => $this]);
}
$transaction->begin();
return $transaction;
}
/**
......
......@@ -64,14 +64,14 @@ class Migration extends \yii\base\Component
$transaction = $this->db->beginTransaction();
try {
if ($this->safeUp() === false) {
$transaction->rollback();
$transaction->rollBack();
return false;
}
$transaction->commit();
} catch (\Exception $e) {
echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n";
echo $e->getTraceAsString() . "\n";
$transaction->rollback();
$transaction->rollBack();
return false;
}
return null;
......@@ -89,14 +89,14 @@ class Migration extends \yii\base\Component
$transaction = $this->db->beginTransaction();
try {
if ($this->safeDown() === false) {
$transaction->rollback();
$transaction->rollBack();
return false;
}
$transaction->commit();
} catch (\Exception $e) {
echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n";
echo $e->getTraceAsString() . "\n";
$transaction->rollback();
$transaction->rollBack();
return false;
}
return null;
......
......@@ -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.
* Note that if the parameter is not a string, it will be returned without change.
* @param string $str string to be quoted
......
......@@ -7,6 +7,7 @@
namespace yii\db;
use Yii;
use yii\base\InvalidConfigException;
/**
......@@ -25,12 +26,12 @@ use yii\base\InvalidConfigException;
* //.... other SQL executions
* $transaction->commit();
* } catch(Exception $e) {
* $transaction->rollback();
* $transaction->rollBack();
* }
* ~~~
*
* @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>
* @since 2.0
......@@ -42,19 +43,18 @@ class Transaction extends \yii\base\Object
*/
public $db;
/**
* @var boolean whether this transaction is active. Only an active transaction
* can [[commit()]] or [[rollback()]]. This property is set true when the transaction is started.
* @var integer the nesting level of the transaction. 0 means the outermost level.
*/
private $_active = false;
private $_level = 0;
/**
* Returns a value indicating whether this transaction is active.
* @return boolean whether this transaction is active. Only an active transaction
* can [[commit()]] or [[rollback()]].
* can [[commit()]] or [[rollBack()]].
*/
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
*/
public function begin()
{
if (!$this->_active) {
if ($this->db === null) {
throw new InvalidConfigException('Transaction::db must be set.');
}
\Yii::trace('Starting transaction', __METHOD__);
$this->db->open();
if ($this->db === null) {
throw new InvalidConfigException('Transaction::db must be set.');
}
$this->db->open();
if ($this->_level == 0) {
Yii::trace('Begin transaction', __METHOD__);
$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.
* @throws Exception if the transaction or the [[db|DB connection]] is not active.
* @throws Exception if the transaction is not active
*/
public function commit()
{
if ($this->_active && $this->db && $this->db->isActive) {
\Yii::trace('Committing transaction', __METHOD__);
if (!$this->getIsActive()) {
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->_active = false;
return;
}
$schema = $this->db->getSchema();
if ($schema->supportsSavepoint()) {
Yii::trace('Release savepoint ' . $this->_level, __METHOD__);
$schema->releaseSavepoint('LEVEL' . $this->_level);
} 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.
* @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) {
\Yii::trace('Rolling back transaction', __METHOD__);
if (!$this->getIsActive()) {
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->_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 {
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
'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.
* A simple table name has no schema prefix.
......
......@@ -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
* natively support transactions.
* @return boolean the result of a transaction rollback.
* @return boolean the result of a transaction roll back.
*/
public function rollBack()
{
......
......@@ -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.
* A simple table name has no schema prefix.
* @param string $name table name.
......
......@@ -34,6 +34,14 @@ class Schema extends \yii\db\Schema
/**
* @inheritdoc
*/
public function releaseSavepoint($name)
{
// does nothing as Oracle does not support this
}
/**
* @inheritdoc
*/
public function quoteSimpleTableName($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