1949啦网--小小 痛苦,是因为能力和欲望不匹配造成的

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/post/4887.html

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

微信扫码关注

更新实时通知

作者:xialibing 分类:网络教程 浏览: