【问题标题】:PHP "Nested" transactions in MySQL could be a reality using the following code?使用以下代码可以实现 MySQL 中的 PHP“嵌套”事务?
【发布时间】:2017-02-09 20:34:23
【问题描述】:

好的,我正在使用 PHP 在 MySQL 中寻找“嵌套”事务的解决方案,正如您在 MySQL 文档中所知道的那样,事务中不可能有事务 (Mysql transactions within transactions)。我试图使用http://php.net/manual/en/pdo.begintransaction.php 中提出的 Database 类,但不幸的是,这对我来说是错误的,因为它的计数器范围是 object level 而不是 class level,来解决这个问题我创建了这个类(TransactionController),它的计数器(名为$nest)是静态的,它带来了使事务“线性”所需的类级别(我说的是“线性”:它显然是嵌套的,但如果你看起来很它不是嵌套的,那么事务将运行良好,你怎么看?(看最后的例子,车主)

        class TransactionController extends \\PDO {
            public static $warn_rollback_was_thrown = false;
            public static $transaction_rollbacked = false;
            public function __construct()
            {
                parent :: __construct( ... connection info ... );
            }
            public static $nest = 0;
            public function reset()
            {
                TransactionController :: $transaction_rollbacked = false;
                TransactionController :: $warn_rollback_was_thrown = false;
                TransactionController :: $nest = 0;
            }
            function beginTransaction()
            {
                $result = null;
                if (TransactionController :: $nest == 0) {
                    $this->reset();
                    $result = parent :: beginTransaction();
                }
                TransactionController :: $nest++;
                return $result;
            }

            public function commit()
            {

                $result = null;

                if (TransactionController :: $nest == 0 &&
                        !TransactionController :: $transaction_rollbacked &&
                        !TransactionController :: $warn_rollback_was_thrown) {
                            $result = parent :: commit();
                        }
                        TransactionController :: $nest--;
                        return $result;
            }

            public function rollback()
            {
                $result = null;
                if (TransactionController :: $nest >= 0) {
                    if (TransactionController :: $nest == 0) {
                        $result = parent :: rollback();
                        TransactionController :: $transaction_rollbacked = true;
                    }
                    else {
                        TransactionController  :: $warn_rollback_was_thrown = true;
                    }
                }
TransactionController :: $nest--;
                return $result;
            }

    public function transactionFailed()
    {
        return TransactionController :: $warn_rollback_was_thrown === true;
    }
    // to force rollback you can only do it from $nest = 0
    public function forceRollback()
    {
        if (TransactionController :: $nest === 0) {
            throw new \PDOException();
}
    }
        }

        class CarData extends TransactionController {
            public function insertCar()
            {

                try {
                    $this->beginTransaction();
                    ... (operations) ...
                    $this->commit();
                }
                catch (\PDOException $e) {
                    $this->rollback();
                }
            }
        }
        class PersonData extends TransactionController {
            public function insertPerson(  $person=null )
            {
                try {
                    $this->beginTransaction();
                    ... (operations) ...
                    $this->commit();
                }
                catch (\PDOException $e) {
                    $this->rollback();
                }
            }
        }

        class CarOwnerData extends TransactionController {
            public function createOwner()
            {
                try {
                    $this->beginTransaction();

                    $car = new CarData();
                    $car->insertCar();

                    $person = new PersonData();
                    $person->insertPerson();

                    ... (operations) ...

                    $this->commit();
                }
                catch (\PDOException $e) {
                    $this->rollback();
                }
            }
        }


        $sellCar = new CarOwnerData();
        $sellCar->createOwner();

UPDATE1static attribute $warn_rollback_was_thrown 被添加到 TransactionController 以警告事务在执行的某个时刻失败,但没有回滚。

UPDATE2:当事务在某个时刻失败时,您可以让代码仍然运行到最后或使用forceRollback() 明确停止它,例如,请参见以下代码:

<?php    // inside the class PersonData

    public function insertMultiplePersons( $arrayPersons )
    {
        try {
        $this->beginTransaction();
        if (is_array( $arrayPersons )) {
            foreach ($arrayPersons as $k => $person) {
                $this->insertPerson( $person ); 
                if ($this->transactionFailed()) {
                    $this->forceRollback();                    
                }
            }
        }
        $this->commit();
        }
        catch (\PDOException $e) {
            $this->rollback();
        }
    } ?>

【问题讨论】:

  • 除了在rollback 之后缺少$nest 的重置:嵌套事务应该支持:start trans 1, do something, start trans 2, rollback trans 2, optionally retry trans 2 or do something else, commit trans 1,你的代码不能支持这个(因为 mysql 不支持它)。如果您从不回滚,您的代码只会按预期工作。每当发生任何内部回滚时,您都需要跳转到 trans 1 的异常(您可以通过重新引发异常来执行此操作),否则在 $person-&gt;insertPerson(); 失败后,代码 ... (operations) ... 将在没有任何事务的情况下执行。
  • 这个想法不是回滚或提交嵌套事务,只是在级别 0 处回滚或提交所有,您需要更好地理解代码。出于这个原因,我说:事务看起来像是嵌套的(php),但实际上在 mysql 执行中它不是嵌套的。看着挺(没有丢失重置,因为有一个$transaction_rollbacked = true
  • 你的意思是,在你编辑之后它会:-P(在它真的没有之前,它只会调用 $parent->rollback,实际上,它仍然不起作用)。但至少现在很清楚你想要做什么。您的代码现在看起来更好了。我认为$this-&gt;beginTransaction(); 应该是$parent-&gt;beginTransaction(); (否则它永远不会调用pdo,而是做一个循环)。 rollback 中的 TransactionController :: $nest--; 必须在 == 0 的测试之前(否则它永远不会回滚),对于 commit 也是如此。并且forceRollback 应该设置一个变量,您在rollback 中检查并重新抛出...
  • 通过使用静态类变量,您刚刚阻止自己在任何给定请求期间拥有多个数据库连接(例如到不同的数据库)。
  • 我将在下面发布一个答案,并附上我的意思的代码示例。

标签: php mysql sql pdo transactions


【解决方案1】:

代码中插入的逻辑必须改变。

不必要的循环是影响性能的最糟糕的事情。

当您知道要插入多个人并且他们可以通过一个查询插入时。不要在循环内做。只需使用一个查询即可。这是多重插入的主要语​​法:

INSERT INTO table_name (col1,col2,col3,...)
VALUES (Value1,Value2,...), (Value1,Value2,...)

insertPerson 方法必须处理多人。像这样:

$this->insertPerson($arrayPersons);

在 insertPerson 方法中,您必须创建 VALUES,就像我之前在这里解释的那样:How to insert multiple dynamic rows into the database

然后,insertPerson 方法可以在 ONE QUERY 中插入一个人或多个人。

【讨论】:

  • 这不能回答问题。
  • 你不能说更好的解决方案不能回答问题,而你想用更复杂和低性能的解决方案来解决问题!让我告诉你一个故事。在肥皂产品线中,出现了故障。一些框空到最后一行。包装机太贵了,他们无力更换或修理它,他们雇了一些人在最后一行之前检查那些盒子。一位智者在包装机导轨和空箱飞出导轨后立即放了一个风扇,因为空箱很轻。也许一个问题有 100 个答案,但请选择快速可靠的答案。
  • 嵌套事务的问题与多次插入的问题无关。 可能还有其他查询需要包含在事务中。
  • @ICE 你的解决方案相当于智者加快包装机的速度,同时空箱在生产中堆积如山。这个“答案”应该是评论。至于“不必要的循环是最糟糕的事情会影响性能”,不确定这是真的。
【解决方案2】:

正如@YourCommonSense 在 cmets 中指出的那样,您实际上并没有实现嵌套事务。

我不确定我是否喜欢在代码中的任何位置调用 commit() 并且它实际上并没有提交任何内容。

您的整个解决方案似乎是试图减轻将事务代码放入您的插入函数并忘记它的设计决策。

您可以将插入操作与事务逻辑分开,并将这些函数调用包装在一个单独的函数中,该函数执行事务:

public/private function insertPerson(  $person=null )
{
  ... (operations) ...
}

public function createPerson()
{
    $person = new Person();
    ... (setup person) ...

    $this->beginTransaction();
    try {
        $this->insertPerson($person);
        $this->commit();
    }
    catch (\PDOException $e) {
        $this->rollback();
    }
} 

如果您绝对确定需要始终在事务中插入人员,则可以在调用事务时检查您是否在事务中:

public/private function insertPerson($person=null)
{
  if (!$this->hasActiveTransaction){ // Needs implementing
     throw new Exception('Must be called within a transaction');
  }
  ...(operations)...
}

在我们的项目中,所有的保存逻辑都在模型中,所有事务逻辑都在控制器级别。

我假设您知道对于单个语句不需要事务,因为这些是原子操作,并且您的代码代表更复杂的情况。

【讨论】:

  • +1 我同意在控制器级别管理事务是在 MVC 架构中有意义的唯一方法。
【解决方案3】:

我在上面发表了评论:

通过使用静态类变量,您刚刚阻止自己在任何给定请求期间拥有多个数据库连接(例如到不同的数据库)。

你似乎对我的评论有疑问:

@BillKarwin 您的意思可能是 $nest 计数器对于每个数据库连接都是静态的。 ——克里斯蒂安·克里什克

这不是static 在 PHP 中的工作方式。静态类属性由类的所有实例共享。如果一个实例更新它,所有其他实例都会看到更改。

<?php

class Foo {
 static $nest = 0;

 public function getNest() {
  return Foo::$nest;
 }

 public function setNest($newNest) {
  Foo::$nest = $newNest;
 }

}

$foo1 = new Foo();
$foo2 = new Foo();

echo "foo1::nest = " . $foo1->getNest() . "\n";
echo "foo2::nest = " . $foo2->getNest() . "\n";

$foo1->setNest(42);

echo "foo1::nest = " . $foo1->getNest() . "\n";
echo "foo2::nest = " . $foo2->getNest() . "\n";

输出:

foo1::nest = 0
foo2::nest = 0
foo1::nest = 42
foo2::nest = 42

这意味着您的静态$nest 类属性对于您的应用程序中的所有数据库连接都是相同的值。因此,您当前的设计不能有多个数据库连接。

我什至不知道您为什么将此属性设为静态。没必要。

但我同意@ICE 的回答,即尝试实现这种“嵌套事务”类是愚蠢的。它不起作用。事务的范围是数据库连接,而不是对象。早在 2008 年,我就曾在 Stack Overflow 上写过这个问题。阅读我的回答:How do detect that transaction has already been started?

【讨论】:

  • 您的回答可能是解决该问题的建议(这显然是正确的),我相信如果您有一个带有连接的静态数组和计数器,它不会产生任何冲突。也许这是考虑这个主题并回答这个问题的最终代码。我正在努力...感谢您的回答。
猜你喜欢
  • 2011-01-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-09
  • 2017-10-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多