MySQLi Transaction Spaghetti Hell

时间:2013-06-27 04:55:55

标签: php transactions mysqli commit autocommit

我有一个包含超过一百个函数/方法的类。其中许多都需要在事务中运行MySQLi查询。因此,在一个简单的形式中,大多数函数看起来像这样:

class MyClass {
    private $connection = ...; // mysqli connection
    public function a () {
        $this->connection->autocommit(FALSE);
        // do something and run multiple queries
        $this->connection->commit();
    }
    public function b () {
        $this->connection->autocommit(FALSE);
        // do something and run multiple queries
        $this->connection->commit();
    }
}
// to run the code:
$myclass = new MyClass();
$myclass->a(); // all queries are run inside a transaction
$myclass->b(); // all queries are run inside a transaction

概念很简单:每次运行公共函数/方法时,它都会启动事务并在结束函数/方法之前提交。 现在,我的问题是......如果我想在另一个方法中运行一个方法,同时保持这两个方法是公共的,并且可以由类外的调用者调用???例如:

class MyClass {
    private $connection = ...; // mysqli connection
    public function a () {
        $this->connection->autocommit(FALSE);
        // do something and run multiple queries
        $this->connection->commit();
    }
    public function b () {
        $this->connection->autocommit(FALSE);
        // do something and run multiple queries
        $this->a(); // running function a inside function b here
        $this->connection->commit();
    }
}
// to run the code:
$myclass = new MyClass();
$myclass->a(); // all queries are run inside a transaction
$myclass->b(); // transaction is not working as I initially wished. This function consists of more than one commit, hence, not everything is run inside one transaction

使用额外的变量进行手动if / else检查,我想我可以用两个函数解决上面的问题。但是,我的问题在于我有多个,可能有近百个函数/方法,其行为如上所述。我不认为我可以使用一些if / else来处理交易意大利面地狱这样复杂的问题。

我该如何解决这个问题?

P.S:

以上只是我班级的简化。为了使事情变得更复杂,在我的课堂上我不仅仅使用:

function func() {
    $this->connection->autocommit(FALSE);
    // do something
    $this->connection->commit();
}

相反,我做了类似的事情:

function func() {
    if ($result = $this->connection->query('SELECT @@autocommit')) {
        $row = mysqli_fetch_row($result);
        $originalAutoCommitValue = $row[0];
        mysqli_free_result($result);
    }
    if (!$originalAutoCommitValue) {
        $this->connection->autocommit(FALSE);
    }
    // do something...
    $this->connection->commit();
    $this->connection->autocommit($originalAutoCommitValue);
}

这将允许我的类中的函数/方法启动事务,提交事务并将自动提交状态返回到之前。这真的很笨拙,但我希望我班级的用户可以选择是否在课外使用事务/提交。我希望这种方法在使用我的任何类方法之前不会影响它们的原始自动提交状态。

更新

我在类中添加了以下属性和两个函数:

private $connectionTransactionStack = 0;
private $connectionOriginalAutoCommitState = NULL;

private function startTransaction() {
    if (is_null($this->connectionOriginalAutoCommitState)) {
        if ($result = $this->connection->query('SELECT @@autocommit')) {
            $row = mysqli_fetch_row($result);
            $this->connectionOriginalAutoCommitState = $row[0];
            mysqli_free_result($result);
        }
    }
    if ($this->connectionOriginalAutoCommitState === 1) {
        $this->connection->autocommit(FALSE);
    }
    $this->connectionTransactionStack++;
}
private function commitTransaction() {
    $this->connectionTransactionStack--;
    if ($this->connectionTransactionStack < 1) {
        $this->connectionTransactionStack = 0;
        if ($this->connectionOriginalAutoCommitState === 1) {
            $this->connection->commit();
            $this->connection->autocommit($this->connectionOriginalAutoCommitState);
        }
    }
}

所以,现在当我想运行原始函数时,我可以这样做:

class MyClass {

    public function a () {
        $this->startTransaction();
        // do something and run multiple queries
        $this->commitTransaction();
    }
    public function b () {
        $this->startTransaction();
        // do something and run multiple queries
        $this->a(); // running function a inside function b here
        $this->commitTransaction();
    }
}

如果我在任何方法中启动任何交易,只要我提交所有交易,非最终$ this-&gt; commitTransaction()将不会运行真正的mysqli-&gt; commit(),但最终的$ this-&gt ; commitTransaction()将在必要时运行mysqli-&gt; commit(取决于第一次运行我的类中的任何方法之前的初始状态)。所以如果我想跑:

$this->a();

$this->b();

,它根本没关系,因为有一个堆栈可以计算我本来要开始的事务。但是,实际上,只有一个事务要启动和提交。

这可以解决我上面的问题吗?

1 个答案:

答案 0 :(得分:1)

一个选项可能是删除

$this->connection->autocommit(FALSE);
$this->connection->commit();
相关的每个函数中的 (a,b) 然后将它们包裹在函数调用周围。

class MyClass {
    private $connection = ...; // mysqli connection
    public function a () {
        // do something and run multiple queries
    }
    public function b () {
        // do something and run multiple queries
        $this->a(); // running function a inside function b here
    }
}
// to run the code:
$myclass = new MyClass();

$myclass->connection->autocommit(FALSE);
$myclass->a(); // all queries are run inside a transaction
$myclass->b(); 
$this->connection->commit();

<强>更新

或者更好,使用包装函数来调用类中的函数:

$myclass->doTransaction('a'); 
$myclass->doTransaction('b'); 

function doTransaction($functionCall) {
    //Do some sanitizing of the functionCall-variable
    $this->connection->autocommit(FALSE);
    call_use_func($functionCall); 
    $this->connection->commit();
}