深入理解Yii2
导读
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事件和关联操作
乐观锁与悲观锁
附录
附录1:Yii2.0 对比 Yii1.1 的重大改进
附录2:Yii的安装
本文档使用 MrDoc 发布
-
+
首页
事务(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还允许把事务嵌套起来使用。 比如,可以采用如下形式来使用事务: ``` $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)_ 部分。 > 而在 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() 。 > 当然,为了使代码功能有一定的层次感,在必要时,也可以使用嵌套的事务。 但要考虑好,子事务是否真的要吞掉异常?有没有必要继续抛出异常, 使得上一层级的事务也产生回滚?这个要根据实际的情形来确定。 ### 提交和回滚 > 提交和回滚通过 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 * 对于嵌套的内层的提交和回滚,使用的其实是事务保存点的释放和回滚 * 释放保存点时,会释放保存点的标识符,这个标识符在下次事务嵌套达到这个层级时, 会被再次使用。 ### 有效的事务 > 在上面的提交、回滚等方法的代码中,我们多次看到了一个 this->getIsActive() 。 这是用于判断当前事务是否有效的一个方法,我们通过它,来看看什么样的一个事务, 算是有效的: ``` public function getIsActive() { return $this->_level > 0 && $this->db && $this->db->isActive; } ``` > 方法很简单明了,一个有效的事务必须同时满足3个条件: * _level > 0 。这是由于为0是,要么是刚刚初始化, 要么是所有的事务已经提交或回滚了。也就是说,只有调用过了 begin() 但还没有调用过匹配的 commit() 或 rollBack() 的事务对象,才是有效的。 * 数据库连接要已经初始化。 * 数据库连接也必须是有效的。
admin
2022年12月19日 14:10
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码