【问题标题】:PDO losing Transaction before commit/rollbackPDO 在提交/回滚之前丢失事务
【发布时间】:2014-09-23 14:17:24
【问题描述】:

我正在尝试使用带有 PDO 的 mysql 的事务来完成这项工作。我遇到的问题是事务在我提交之前就中断了。我知道这一点,因为我在连接上回显了 inTransaction() 函数。

在我看来,这样做的原因是我正在实例化一个 PDODatabase 类,然后在实际执行任何查询之前我做了一些其他编码工作,而此时我丢失了事务。

实例化我的类

$pdo = new PdoDatabase;
$pdo->beginTransaction();
echo "first ".$pdo->transactionStarted()."<br />";

PdoDatabase 类

public function __construct(){
    $dsn = 'mysql:host='.DB_HOST.';dbname='.DB_NAME;
    $options = array(PDO::ATTR_PERSISTENT => TRUE, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION);
    try{
        $this->_connection = new PDO($dsn, DB_USER, BD_PASS, $options);
    } catch (PDOException $e){
        $this->_error = $e->getMessage();
    }
}

public function query($q){
    if($this->_error != ''){
        echo $this->_error;
    } else {
        $this->_stmt = $this->_connection->prepare($q);
    }
}

public function bind($param, $value, $type = null){
    //echo "<br>".$value."<br>";
    if (is_null($type)) {
      switch (true) {
        case is_int($value):
          $type = PDO::PARAM_INT;
          break;
        case is_bool($value):
          $type = PDO::PARAM_BOOL;
          break;
        case is_null($value):
          $type = PDO::PARAM_NULL;
          break;
        default:
          $type = PDO::PARAM_STR;
      }
    }
    $this->_stmt->bindValue($param, $value, $type);
}

public function execute($class = null){
    $object_array = array();

    if($class !== null){
        if($this->_stmt->execute()){
            $this->_stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, $class, null);
            while($row = $this->returnRow()){
                $object_array[] = $class::instantiate($row);
            }
        }
        return $object_array;
    } else {

        return $this->_stmt->execute();
    }
}

public function transactionStarted(){
    return $this->_connection->inTransaction();
}

我是如何使用它的(部分)

if(isset($_POST['id']) && $_POST['id'] != ''){
                echo "id exists ".$pdo->transactionStarted()."<br />";
                $bidder->getBidderById($_POST['id']);
                echo "step 1 ".$pdo->transactionStarted()."<br />";
                $bidder = $bidder->getList(0);
                echo "step 2 ".$pdo->transactionStarted()."<br />";
                $old_bidder = clone $bidder;
                echo "step 3 ".$pdo->transactionStarted()."<br />";

                $bidder_phones->getPhones($_POST['id']);
                echo "step 4 ".$pdo->transactionStarted()."<br />";
                $bidder_phones = $bidder_phones->getList();
                echo "step 5 ".$pdo->transactionStarted()."<br />";
                if($_POST['phone'] == ''){
                    // check to see if there are any phone numbers in the database already and delete if there is
                    foreach($bidder_phones as $bp){
                        $q = "delete from bidder_phones where id = :id";
                        $pdo->query($q);
                        $pdo->bind(":id", $bp->getId());
                        $pdo->execute();
                        //$bp->remove();
                    }
                } else {
                    echo "phone to check ".$pdo->transactionStarted()."<br />";
                    $old_phone_numbers = array();
                    $new_phone_numbers = explode(',', $_POST['phone']);
                    foreach($bidder_phones as $bp){
                        // remove any unused phone numbers
                        if(!in_array($bp, $new_phone_numbers)){
                            $q = "delete from bidder_phones where id = :id";
                            $pdo->query($q);
                            $pdo->bind(":id", $bp->getId());
                            $pdo->execute();
                            //$bp->remove();
                        }
                        // push to an array to test the new numbers
                        array_push($old_phone_numbers, $bp->getPhone());
                    }
                    foreach($new_phone_numbers as $phone){
                        // adding new phone numbers
                        if(!in_array($phone, $old_phone_numbers)){
                            $new_phone = new BidderPhone;
                            $new_phone->setPhone($phone);
                            $new_phone->setBidderId($_POST['id']);
                            $pdo->save('BidderPhones', $new_phone);
                            //$new_phone->save();
                        }
                    }
                }

如您所见,我多次回显 $pdo->transactionStarted()。这是试图查看我何时丢失交易。我期望看到的是我的消息后跟一个 1 以表明该事务仍然处于活动状态。这是我得到的:

first 1
id exists 1
step 1
step 2
step 3
step 4
step 5
phone to check
in save
before create

Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42S02]: Base table or view not found: 1146 Table 'clickbid.bidder_phoneses' doesn't exist' in /var/www/classes/PdoDatabase.php:59 Stack trace: #0 /var/www/classes/PdoDatabase.php(59): PDOStatement->execute() #1 /var/www/classes/PdoDatabase.php(158): PdoDatabase->execute() #2 /var/www/classes/PdoDatabase.php(187): PdoDatabase->getFields('BidderPhones') #3 /var/www/classes/PdoDatabase.php(176): PdoDatabase->create('BidderPhones', Object(BidderPhone), false) #4 /var/www/admin/data/post_data.php(284): PdoDatabase->save('BidderPhones', Object(BidderPhone)) #5 {main} thrown in /var/www/classes/PdoDatabase.php on line 59

所以我在 id 存在后立即丢失了事务,这是因为我正在执行其他编程而不是准备查询并执行它们吗?还有什么我需要知道的关于我失踪的事情吗?在过去两天的大部分时间里,我一直在努力解决这个问题。问题是我真的需要能够在实际执行一些查询之前开始我的事务并做一些工作。有没有办法做到这一点?

提前感谢您的帮助。

编辑 因此,我对代码进行了一些重大更改,并且能够更准确地确定何时丢失活动事务。

我的保存方法是这样的

public function save($table, $object, $old_object = null){
    echo "in save ".$this->transactionStarted()."<br />";
    if($object->_id != ''){
        echo "before update ".$this->transactionStarted()."<br />";
        //return $this->update($table, $object, $old_object, $transaction);
        if($this->update($table, $object, $old_object)){
            echo "update true ".$this->transactionStarted()."<br />";
            return true;
        } else {
            echo "update false ".$this->transactionStarted()."<br />";
            return false;
        }
    } else {
        echo "before create ".$this->transactionStarted()."<br />";
        //return $this->create($table, $object, $transaction);
        if($this->create($table, $object)){
            echo "create true ".$this->transactionStarted()."<br />";
            return true;
        } else {
            echo "create false ".$this->transactionStarted()."<br />";
            return false;
        }
    }
}

在这种特殊情况下,有一个来自对象的 _id,所以 $this->update 是我们正在调试的,这里是更新方法

private function update($table, $object, $old){
    echo "in update ".$this->transactionStarted()."<br />";
    $audit = new PdoDatabase;
    echo "after new ".$this->transactionStarted()."<br />";
    $aq = "insert into audit_trails 
            (table_name, table_id, user_id, field_name, original_value, new_value) values
            (:table_name, :table_id, :user_id, :field_name, :original_value, :new_value)";
    echo "before query ".$this->transactionStarted()."<br />";
    $audit->query($aq);
    echo "after query ".$this->transactionStarted()."<br />";
    //$update = new PdoDatabase;    

    $binding = array();
    echo "before field_names ".$this->transactionStarted()."<br />";
    $field_names = self::getFields($table);
    echo "after field_names ".$this->transactionStarted()."<br />";

    $uc = new UserConfig;
    $uc->getConfig();
    $user = $uc->getList(0);
    echo "before foreach ".$this->transactionStarted()."<br />";
    foreach($field_names as $field){
        $thisField = "_".$field['Field']."<br>";
        $getField = 'get'.self::to_camel_case($field['Field']);
        $method = $getField;
        $class = self::to_camel_case($table);
        $field_list = '';

        if($field['Field'] == 'id'){
            $where = 'where id = :id';
            $binding[':id'] = ($object->$getField());
        } else {

            if(method_exists($class, $method)){
                if($object->$getField() != $old->$getField()){
                    $field_list .= $field['Field']."= :".$field['Field'].", ";
                    $binding[':'.$field['Field']] = $object->$getField();

                    $audit->bind(':table_name', $table);
                    $audit->bind(':table_id', $object->getId());
                    $audit->bind(':user_id', $user->getUserId());
                    $audit->bind(':field_name', $thisField);
                    $audit->bind(':original_value', $object->$getField());
                    $audit->bind(':new_value', $old->$getField());

                    echo "before audit execute ".$this->transactionStarted()."<br />";
                    $audit->execute();
                    echo "after audit execute ".$this->transactionStarted()."<br />";

                }
            }
        }
    }
    echo "before binding ".$this->transactionStarted()."<br />";
    if(count($binding) > 1){
        $q = "update ".self::singularToPlural($table)." set ";
        foreach($binding as $key => $value){
            if($key != ':id'){
                $q .= str_replace(':', '', $key)." = ".$key.", ";
            }
        }
        $q = rtrim($q, ", ");
        $q .= ' '.$where;

        //$update->query($q);
        echo "before this query ".$this->transactionStarted()."<br />";
        $this->query($q);
        echo "after this query ".$this->transactionStarted()."<br />";
        /*if($transaction && !$this->_stmt->inTransaction()){
            $this->_stmt->beginTransaction();
        }*/

        foreach($binding as $key => $value){
            //$update->bind($key, $value);
            $this->bind($key, $value);
        }
        //$update->bind($id);
        //return $update->execute();
        echo "before this execute ".$this->transactionStarted()."<br />";
        $stupid = $this->execute();
        echo "after this execute ".$this->transactionStarted()."<br />";
        return $stupid;
    } else {
        echo "before return true ".$this->transactionStarted()."<br />";
        return true;
    }

}

输出是

first 1
in save 1
before update 1
in update 1
after new 1
before query 1
after query 1
before field_names 1
before execute 1
after execute 1
after field_names 1
before foreach 1
before audit execute 1
before execute 1
after execute 1
after audit execute 1
before binding 1
before this query 1
after this query 1
before this execute 1
before execute 1
after execute 1
after this execute 1
update true
the save 1
second
after save
before commit

Fatal error: Uncaught exception 'PDOException' with message 'There is no active transaction' in /var/www/classes/PdoDatabase.php:86 Stack trace: #0 /var/www/classes/PdoDatabase.php(86): PDO->commit() #1 /var/www/admin/data/post_data.php(403): PdoDatabase->commit() #2 {main} thrown in /var/www/classes/PdoDatabase.php on line 86

从这里我可以看到,当我们从更新返回到保存方法时,活动事务丢失了,我只是不确定这是为什么。

再次感谢。

编辑添加 getBidderById 函数

public function getBidderById($bidder_id){
    $pdo = new PdoDatabase;

    $q = "select *
            from bidders Bidder
            where Bidder.id = :bidder_id
            limit 1";

    $pdo->query($q);
    $pdo->bind(":bidder_id", $bidder_id);
    $this->_bidders = $pdo->execute('Bidder');

    if(!empty($this->_bidders)){
        return true;
    } else {
        return false;
    }
}

编辑添加创建方法

private function create($table, $object){
    //$insert = new PdoDatabase;

    $field_names = self::getFields($table);

    foreach($field_names as $field){
        $getField = 'get'.self::to_camel_case($field['Field']);
        $method = $getField;
        $class = self::to_camel_case($table);

        if(method_exists($class, $method)){
            if($field['Field'] != 'id'){
                $fields = $field['Field'].", ";
                $binding_fields = ":".$field['Field'].", ";
                $binding[':'.$field['Field']] = $object->$getField();
            }
        }
    }
    $fields = rtrim($fields, ", ");
    $binding_fields = rtrim($binding_fields, ", ");

    $iq = "insert into ".self::singularToPlural($table)." 
            (".$fields.") values 
            (".$binding_fields.")";
    $this->query($iq);
    /*if($transaction && !$this->_stmt->inTransaction()){
        $this->_stmt->beginTransaction();
    }*/

    foreach($binding as $key => $value){
        $this->bind($key, $value);
    }

    if($this->execute()){
        $object->setId($this->getConnection()->lastInsertId());
        return true;
    } else {
        return false;
    }

}

【问题讨论】:

  • $bidder-
  • $bidder->getBidderById($_POST['id']) 查询该投标人的数据库,它填充投标人实例,如果有任何不同,稍后将使用该实例更新投标人,因此 $ old_bidder = 克隆 $bidder。这两者都被传递给 save 方法并相互比较以确定需要更新的内容。
  • 没有涉及到 DDL 语句吗?,你能展示一下函数吗?
  • 你有一个未捕获的致命错误,当然你会“丢失”你的事务 - 当 PHP 关闭时,它会关闭数据库连接,这将回滚所有事务。
  • 提交导致了致命错误。发生致命错误是因为我在提交之前“丢失”了事务。

标签: php mysql sql pdo transactions


【解决方案1】:

嗯,我注意到你在一个函数中实例化 PdoDatabase 并且你使用了 $this->transactionStarted()。

可能不是问题,但由于您使用的是持久连接,因此您可能会在其他类中启动新事务,这会导致正在运行的事务的隐式提交。

    if($this->create($table, $object)){
        echo "create true ".$this->transactionStarted()."<br />";
        return true;
    }

如果在此函数中使用 DDL 语句(CREATE、ALTER、DROP、TRUNCATE 等),也会应用提交

【讨论】:

  • 我明白你的意思,但我不是在那个时候失去活动交易,我怀疑会这样做,而是在我回到原来的电话的时候。
  • create 没有被使用,这只是我的方法名称。在这个方法中唯一要做的就是插入我会在一秒钟内将它添加到我上面的帖子中。
【解决方案2】:

MySQL 中的某些语句会导致隐式提交。您可能正在进行的任何事务都已提交。例如,所有 DDL 语句都这样做。有关更多信息,请参阅https://dev.mysql.com/doc/refman/5.6/en/implicit-commit.html

很难知道您的会话中是否有活动事务。在导致隐式提交的语句和查询可以是“提交”的可能性之间(不通过任何 commit() 函数,而是通过 query() 函数),客户端无法确定它们是否仍然有交易进行中。

这个问题是试图创建可重用 DBAL 的开发人员的祸根。

另见我对How do detect that transaction has already been started?的回复


重新评论:

对于 MySQL,PDO::inTransaction()可靠,因为 PDO mysql 驱动程序没有实现方法 mysql_handle_in_transaction()MySQL C API 不支持查询当前会话的事务状态。

因此,PDO 类尝试使用内部变量进行最佳猜测,当您调用 $dbh-&gt;beginTransaction() 时该变量设置为 1,而当您调用 $dbh-&gt;commit()$dbh-&gt;rollback() 时该变量设置为 0。

但是,如果 DDL 语句导致隐式提交,则驱动程序不会收到有关事务结束的任何通知。因此 PDO 中的内部变量可能与现实不同步。如果您调用 $dbh-&gt;query('COMMIT'),绕过 PDO 函数进行事务控制,也会发生这种情况。

其他 PDO 驱动程序,例如 PostgreSQL 驱动程序,确实实现了获取当前事务状态的方法。


在这种情况下,我并不是说这是您的问题的原因。但是尝试检测客户端中的事务状态并不总是有效。

我无法判断您的情况。您仍然有几行未共享代码的调试输出。

【讨论】:

  • 所以你是说连接的inTransaciton()方法不可靠?因为我正在使用它并且它似乎对我有用,所以如果我在交易中,它会返回 1(或 true)。从我上面所有的例子中可以看出它正在发生。这就是为什么我觉得在我执行查询语句并返回给调用者之后事务正在中断,这对我来说没有意义,因为除了返回 true 之外什么都没有发生(假设执行成功)并且事务仍然处于活动状态返回前
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-18
  • 2013-05-31
  • 2017-02-14
  • 2011-01-26
  • 2012-12-24
  • 2013-11-20
相关资源
最近更新 更多