期权记-ETF期权开户导航 期权记是专业的50ETF300ETF期权开户投资交易导航

Yii2中的事务

今天运行程序时发现有条数据不完整。出现问题的数据属于某个事务,按道理要么逻辑走完数据提交,要么回滚。出现预料外问题,第一个反应是ActiveRecord中内嵌事务会单独提交到数据库中?为了验证这个问题,抽空写了一个测试用例验证。

准备工作

先建立两个表 foo1foo2

CREATE TABLE `foo1` (   `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,   `data1` varchar(12) NOT NULL UNIQUE,   `value` varchar(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  CREATE TABLE `foo2` (   `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,   `data2` varchar(12) NOT NULL UNIQUE,   `value` varchar(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

创建相应的ActiveRecord类,并定义好规则:

// file: Foo1.php namespace console\models;  use yii\db\ActiveRecord;  class Foo1 extends ActiveRecord {     public function rules()     {         return [             [["data1", "value"], "required", "on" => [self::SCENARIO_DEFAULT, "other]],             ["data1", "string", "length" => [2, 8]],         ];     }      public function transactions()     {         return [             self::SCENARIO_DEFAULT => self::OP_ALL,         ];     } }  // file: Foo2.php namespace console\models;  use yii\db\ActiveRecord;  class Foo2 extends ActiveRecord {     public function rules()     {         return [             [["data2", "value"], "required"],             ["data2", "string", "length" => [2, 8]],         ];     }      public function transactions()     {         return [             self::SCENARIO_DEFAULT => self::OP_ALL,         ];     } } 

编写测试用例

为了彻底搞清楚Yii2中事务的执行情况,总共编写了六个例子。六个示例的作用分别是:

  1. 非事务保存、数据校验不通过
  2. 事务保存、数据校验不通过
  3. 校验通过、多模型数据保存
  4. 某条数据校验不通过
  5. 某条数据插入冲突
  6. 事务执行中exit/return

测试例子的代码如下:

namespace console\controllers;  use Yii; use yii\db\Exception; use yii\console\Controller; use console\models\{Foo1, Foo2};  class TestController extends Controller {     public function beforeAction($action)     {         if (parent::beforeAction($action)) {           $db = Foo1::getDb();           $db->createCommand("truncate table " . Foo1::tableName())->execute();           $db->createCommand("truncate table " . Foo2::tableName())->execute();           return true;         }         return false;     }      public function actionCase1()     {         $transaction = Yii::$app->db->beginTransaction();         try {             $foo1 = new Foo1([                 "data1" => "1234567890",                 "value" => "1245677553",             ]);             $foo1->scenario = 'other';             $foo1->save();             $transaction->commit();              echo "transaction committed";         } catch (Exception $e) {             $transaction->rollback();             echo "insert data error:", $e->getMessage();         }     }      public function actionCase2()     {         $transaction = Yii::$app->db->beginTransaction();         try {             $foo1 = new Foo1([                 "data1" => "1234567890",                 "value" => "1245677553",             ]);             $foo1->save();             $transaction->commit();              echo "transaction committed";         } catch (Exception $e) {             $transaction->rollback();             echo "insert data error:", $e->getMessage();         }     }      public function actionCase3()     {         $transaction = Yii::$app->db->beginTransaction();         try {             $foo1 = new Foo1([                 "data1" => "12345678",                 "value" => "1245677553",             ]);             $foo1->save();             $foo2 = new Foo1([                 "data1" => "12345678",                 "value" => "1245677553",             ]);             $foo2->save();             $transaction->commit();              echo "transaction committed";         } catch (Exception $e) {             $transaction->rollback();             echo "insert data error:", $e->getMessage();         }     }      public function actionCase4()     {         $transaction = Yii::$app->db->beginTransaction();         try {             $foo1 = new Foo1([                 "data1" => "12345678",                 "value" => "1245677553",             ]);             $foo1->save();              $foo2 = new Foo2([                 "data2" => "1234567890",                 "value" => "1245677553",             ]);             $foo2->save();             $transaction->commit();              echo "transaction committed";         } catch (Exception $e) {             $transaction->rollback();             echo "insert data error:", $e->getMessage();         }     }      public function actionCase5()     {         $transaction = Yii::$app->db->beginTransaction();         try {             $foo1 = new Foo1([                 "data1" => "12345678",                 "value" => "1245677553",             ]);             $foo1->save();              $foo2 = new Foo2([                 "data2" => "12345678",                 "value" => "1245677553",             ]);             $foo2->save();              $foo2 = new Foo2([                 "data2" => "12345678",                 "value" => "1245677553",             ]);             $foo2->save();             $transaction->commit();              echo "transaction committed";         } catch (Exception $e) {             $transaction->rollback();             echo "insert data error:", $e->getMessage();         }     }      public function actionCase6()     {         $transaction = Yii::$app->db->beginTransaction();         echo "transaction level:", $transaction->level, PHP_EOL;         try {             $foo1 = new Foo1([                 "data1" => "12345678",                 "value" => "1245677553",             ]);             $foo1->save();              echo "exit now";             exit;             $transaction->commit();         } catch (Exception $e) {             $transaction->rollback();             echo "insert data error:", $e->getMessage();         }     } } 

执行结果

依次执行上述测试用例,结果如下:

  • case1: 输出”transaction committed”,数据未插入;
  • case2: 输出”transaction committed”,数据未插入;
  • case3: 输出”insert data error:SQLSTATE[23000]: Integrity constraint violation:1062 Duplicate entry ‘12345678’ for key ‘data1’ The SQL being executed was: INSERT INTO foo1 (data1, value) VALUES (‘12345678’, ‘1245677553’)”,数据未插入;
  • case4: 输出”transaction committed”,foo1中的数据成功插入;
  • case5: 输出”insert data error:SQLSTATE[23000]: Integrity constraint violation:1062 Duplicate entry ‘12345678’ for key ‘data2’ The SQL being executed was: INSERT INTO foo2 (data2, value) VALUES (‘12345678’, ‘1245677553’)”,数据未插入;
  • case6: 输出”exit now”,数据未插入。

分析

大部分示例的结果在预料之中,震惊的是case2和case4的结果。之前一直以为只要包裹在事务中,并且在transactions方法中声明了所在场景启用事务,数据保存出错就会抛异常,数据回滚。这个测试彻底颠覆了我的认知。

为了搞清楚执行机制,开始跟踪Yii2执行数据保存的源码。首先查看ActiveRecord基类BaseActiveRecord中的save方法:

public function save($runValidation = true, $attributeNames = null) {     if ($this->getIsNewRecord()) {         return $this->insert($runValidation, $attributeNames);     } else {         return $this->update($runValidation, $attributeNames) !== false;     } } 

save方法根据是否新数据,走插入或更新流程。继续跟踪insert方法(定义在yii\db\ActiveRecord中):

public function insert($runValidation = true, $attributes = null) {     if ($runValidation && !$this->validate($attributes)) {         Yii::info('Model not inserted due to validation error.', __METHOD__);         return false;     }      if (!$this->isTransactional(self::OP_INSERT)) {         return $this->insertInternal($attributes);     }      $transaction = static::getDb()->beginTransaction();     try {         $result = $this->insertInternal($attributes);         if ($result === false) {             $transaction->rollBack();         } else {             $transaction->commit();         }         return $result;     } catch (\Exception $e) {         $transaction->rollBack();         throw $e;     } catch (\Throwable $e) {         $transaction->rollBack();         throw $e;     } } 

insert方法的实现代码解决了我的疑问:数据的规则验证不通过,直接返回false,不会抛异常。

再看保存过程:如果当前场景未声明事务,常规保存;事务保存第一步还是尝试常规保存,如果失败,回滚并抛出异常;如果事务保存成功,提交事务。

到这一步,Yii中事务处理已经比较清晰了。剩下的问题是:嵌套事务如何处理?继续跟踪yii\db\Transaction中的commit方法:

public function commit() {     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->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__);     } } 

代码中出现事务的层级(level),结合begin方法,每嵌套一层事务,level加一并创建savepoint。事务提交时,如果是最外层事务,直接提交到数据库;如果是内嵌事务,释放savepoint或什么都不做。所以嵌套事务的疑问也解决了:内嵌事务不会单独提交。

总结

通过这次测试和源码跟踪阅读,对Yii的事务了解又深入一步。最大的收获是:事务开始前调用validate方法先校验数据,无错误时再通过事务中调用save(false)方法插入数据,此时出错才会抛出异常。

本站声明:网站内容来源于网络,如有侵权,请联系我们https://www.qiquanji.com,我们将及时处理。

微信扫码关注

更新实时通知

作者:期权小韭菜 分类:网络教程 浏览:
请先 登录 再评论,若不是会员请先 注册