diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 14eb2e62a6..ea764b1814 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -68,6 +68,7 @@ Yii Framework 2 Change Log - Enh #3132: `yii\rbac\PhpManager` now supports more compact data file format (qiangxue) - Enh #3154: Added validation error display for `GridView` filters (ivan-kolmychek) - Enh #3196: Masked input upgraded to use jquery.inputmask plugin with more features. (kartik-v) +- Enh #3220: Added support for setting transation isolation levels (cebe) - Enh #3222: Added `useTablePrefix` option to the model generator for Gii (horizons2) - Enh #3230: Added `yii\filters\AccessControl::user` to support access control with different actors (qiangxue) - Enh #3232: Added `export()` and `exportAsString()` methods to `yii\helpers\BaseVarDumper` (klimov-paul) diff --git a/framework/db/Connection.php b/framework/db/Connection.php index a9638e7c22..1388837281 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -410,16 +410,18 @@ class Connection extends Component /** * Starts a transaction. + * @param string|null $isolationLevel The isolation level to use for this transaction. + * See [[Transaction::begin()]] for details. * @return Transaction the transaction initiated */ - public function beginTransaction() + public function beginTransaction($isolationLevel = null) { $this->open(); if (($transaction = $this->getTransaction()) === null) { $transaction = $this->_transaction = new Transaction(['db' => $this]); } - $transaction->begin(); + $transaction->begin($isolationLevel); return $transaction; } diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 57207345ec..a2c7bfda94 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -339,6 +339,19 @@ abstract class Schema extends Object $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute(); } + /** + * Sets the isolation level of the current transaction. + * @param string $level The transaction isolation level to use for this transaction. + * This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]], [[Transaction::REPEATABLE_READ]] + * and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific syntax to be used + * after `SET TRANSACTION ISOLATION LEVEL`. + * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + */ + public function setTransactionIsolationLevel($level) + { + $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level;")->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. diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index f524604d82..39a2e47b38 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -38,6 +38,27 @@ use yii\base\InvalidConfigException; */ class Transaction extends \yii\base\Object { + /** + * A constant representing the transaction isolation level `READ UNCOMMITTED`. + * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + */ + const READ_UNCOMMITTED = 'READ UNCOMMITTED'; + /** + * A constant representing the transaction isolation level `READ COMMITTED`. + * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + */ + const READ_COMMITTED = 'READ COMMITTED'; + /** + * A constant representing the transaction isolation level `REPEATABLE READ`. + * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + */ + const REPEATABLE_READ = 'REPEATABLE READ'; + /** + * A constant representing the transaction isolation level `SERIALIZABLE`. + * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + */ + const SERIALIZABLE = 'SERIALIZABLE'; + /** * @var Connection the database connection that this transaction is associated with. */ @@ -47,6 +68,7 @@ class Transaction extends \yii\base\Object */ private $_level = 0; + /** * Returns a value indicating whether this transaction is active. * @return boolean whether this transaction is active. Only an active transaction @@ -59,9 +81,15 @@ class Transaction extends \yii\base\Object /** * Begins a transaction. + * @param string|null $isolationLevel The [isolation level][] to use for this transaction. + * This can be one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but + * also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. + * If not specified (`null`) the isolation level will not be set explicitly and the DBMS default will be used. + * + * [isolation level]: http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels * @throws InvalidConfigException if [[db]] is `null`. */ - public function begin() + public function begin($isolationLevel = null) { if ($this->db === null) { throw new InvalidConfigException('Transaction::db must be set.'); @@ -69,7 +97,10 @@ class Transaction extends \yii\base\Object $this->db->open(); if ($this->_level == 0) { - Yii::trace('Begin transaction', __METHOD__); + if ($isolationLevel !== null) { + $this->db->getSchema()->setTransactionIsolationLevel($isolationLevel); + } + Yii::trace('Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : ''), __METHOD__); $this->db->pdo->beginTransaction(); $this->_level = 1; @@ -141,4 +172,25 @@ class Transaction extends \yii\base\Object throw new Exception('Roll back failed: nested transaction not supported.'); } } + + /** + * Sets the transaction isolation level for this transaction. + * + * This method can be used to set the isolation level while the transaction is already active. + * However this is not supported by all DBMS so you might rather specify the isolation level directly + * when calling [[begin()]]. + * @param string $level The transaction isolation level to use for this transaction. + * This can be one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but + * also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. + * @throws Exception if the transaction is not active + * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + */ + public function setIsolationLevel($level) + { + if (!$this->getIsActive()) { + throw new Exception('Failed to set isolation level: transaction was inactive.'); + } + Yii::trace('Setting transaction isolation level to ' . $level, __METHOD__); + $this->db->getSchema()->setTransactionIsolationLevel($level); + } } diff --git a/framework/db/sqlite/Schema.php b/framework/db/sqlite/Schema.php index bcc83b5e51..eecdfc3161 100644 --- a/framework/db/sqlite/Schema.php +++ b/framework/db/sqlite/Schema.php @@ -7,8 +7,10 @@ namespace yii\db\sqlite; +use yii\base\NotSupportedException; use yii\db\TableSchema; use yii\db\ColumnSchema; +use yii\db\Transaction; /** * Schema is the class for retrieving metadata from a SQLite (2/3) database. @@ -249,4 +251,27 @@ class Schema extends \yii\db\Schema return $column; } + + /** + * Sets the isolation level of the current transaction. + * @param string $level The transaction isolation level to use for this transaction. + * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]]. + * @throws \yii\base\NotSupportedException when unsupported isolation levels are used. + * SQLite only supports SERIALIZABLE and READ UNCOMMITTED. + * @see http://www.sqlite.org/pragma.html#pragma_read_uncommitted + */ + public function setTransactionIsolationLevel($level) + { + switch($level) + { + case Transaction::SERIALIZABLE: + $this->db->createCommand("PRAGMA read_uncommitted = False;")->execute(); + break; + case Transaction::READ_UNCOMMITTED: + $this->db->createCommand("PRAGMA read_uncommitted = True;")->execute(); + break; + default: + throw new NotSupportedException(get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'); + } + } }