使用以下代码,MySQL中的PHP“嵌套”事务可能成为现实吗?

3

我正在寻找一种使用PHP在MySQL中解决“嵌套”事务的方法,正如你所知,在MySQL文档中写道,不可能在事务内再开启一个事务 (Mysql transactions within transactions)。我尝试使用http://php.net/manual/en/pdo.begintransaction.php中建议的Database类,但不幸的是,这对我来说是错误的,因为它的计数器范围是对象级别而不是类级别。为了解决这个问题,我创建了这个类(TransactionController),它的计数器(命名为$nest)是静态的,并且它带来了必要的类级别,以使事务“线性化”(所谓的“线性化”是指:看起来是嵌套的,但如果你仔细看它并没有嵌套),那么事务将正常工作,你认为呢?(请参考结尾的示例,CarOwner)

        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();

更新1: 在TransactionController中添加了静态属性$warn_rollback_was_thrown,以便在执行某些时刻事务失败但未回滚时发出警告。

更新2: 当事务在某个时刻失败时,您可以让代码继续运行到结束,也可以使用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();
        }
    } ?>

1
除了在回滚后没有重置$nest之外:嵌套事务应该支持以下操作:start trans 1, 做一些事情, start trans 2, rollback trans 2, 可选地重试 trans 2 或者做其他事情, commit trans 1,你的代码不能支持这样的操作(因为mysql不支持)。只有当内部回滚发生时,你才需要跳转到 trans 1 的异常处理部分(你可以通过重新引发异常来实现),否则在 $person->insertPerson(); 失败后,代码 ... (operations) ... 将会在没有任何事务的情况下执行。 - Solarflare
你的意思是,经过你的修改它现在可以工作了 :-P (之前它真的不行,只会调用 $parent->rollback,实际上,现在仍然不行)。但至少现在清楚你想做什么了。你的代码看起来更好了。我认为 $this->beginTransaction(); 应该改成 $parent->beginTransaction();(否则它永远不会调用 pdo,而是进入一个循环)。在 rollback 中的 TransactionController :: $nest--; 必须在测试 == 0 之前(否则它永远不会回滚),commit 同理。并且 forceRollback 应该设置一个变量,在 rollback 中检查并重新抛出... - Solarflare
如果 $nest > 0(否则它只会强制向下一层,除非你想要那样),则可以再次使用。在某处您可能想要添加一个 $next < 0 的检查。在您的第一个启动之前简单地忘记了 commit 将不会引发错误,但是在代码的其余部分中行为不正确(提交级别1)。在 rollback 中您可能仍然需要一个 reset(并且出于安全起见,在 commit 中也需要),否则您无法在级别0上开始新的事务-至少如果您希望它能够这样做的话。并且由于重新抛出异常不是默认行为,因此您需要在 commit 中进行检查,如果... - Solarflare
1
通过使用静态类变量,您刚刚阻止了在任何给定请求期间拥有多个数据库连接(例如到不同数据库)的可能性。 - Bill Karwin
1
我会在下面发布一个答案,其中包含我所说的代码示例。 - Bill Karwin
显示剩余8条评论
3个回答

4

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

我不确定我是否喜欢在我的代码任何地方调用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)...
}

在我们的项目中,所有的保存逻辑都在模型(Model)中处理,而所有事务逻辑则在控制器(Controller)层处理。
我猜您应该知道,对于单个语句来说,并不需要使用事务,因为它们是原子操作。但是您的代码可能涉及到更复杂的情况。

+1 我同意在MVC架构中,仅通过控制器级别来管理事务是唯一有意义的方式。 - Bill Karwin

2
我在上面发表了评论:
通过使用静态类变量,您刚刚阻止自己在任何给定请求期间拥有多个db连接(例如到不同数据库)。
您似乎对我的评论有疑问:
@BillKarwin您的意思可能是$nest计数器对每个db连接都是静态的。-Cristian Crishk
这不是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?

你的回答可能是解决这个问题的建议(显然是正确的),我相信如果你有一个带有连接和计数器的静态数组,它将不会产生任何冲突。也许这就是考虑了这个问题并回答了这个问题的最终代码。我正在努力中...感谢你的回答。 - Sequoya

0

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

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

当您知道要插入多个人并且可以使用一个查询插入时,请不要在循环内执行。只需使用一个查询即可完成。这是用于多次插入的主要语法:

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

insertPerson方法必须处理多个人,就像这样:

$this->insertPerson($arrayPersons);

在insertPerson方法内,您必须创建与我之前解释的如何将多个动态行插入数据库相似的VALUES。

之后,insertPerson方法可以在一个查询中插入一个或多个人。


2
这并没有回答问题。 - Your Common Sense
当你想用更复杂和低性能的解决方案来解决问题时,你不能说更好的解决方案无法回答问题!让我给你讲个故事。在一个肥皂产品线上,出现了故障。有些盒子到了最后一道生产线是空的。包装机非常昂贵,他们无法承担更换或修理它,于是他们雇了一些人在最后一道生产线之前检查这些盒子。一个聪明的人在包装机轨道后面放了一个风扇,因为空盒子非常轻,所以空盒子就从轨道上飞出去了。也许一个问题有100个答案,但要选择快速可靠的解决方案。 - ICE
1
嵌套事务的问题与多个插入的问题无关。可能还有其他查询需要包含在事务中。 - Your Common Sense
@ICE 你的解决方案就像是聪明人加速了包装机器,与此同时生产线上空箱子越来越多。这个“答案”应该是一条评论。至于“不必要的循环是影响性能最糟糕的事情”,我不确定这是否正确。 - Arth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接