Yii2中的事务

栏目: PHP · 发布时间: 8年前

内容简介: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) 方法插入数据,此时出错才会抛出异常。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Clean Code

Clean Code

Robert C. Martin / Prentice Hall / 2008-8-11 / USD 49.99

Even bad code can function. But if code isn’t clean, it can bring a development organization to its knees. Every year, countless hours and significant resources are lost because of poorly written code......一起来看看 《Clean Code》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具