数据库插入不在嵌套事务中执行

时间:2014-02-19 10:59:46

标签: php pdo transactions

我想检查用户是否更改了编辑表单中的Document 图片。如果用户更改了图像,我必须从数据库和文件系统中删除旧的,然后我必须添加新的(在db和filesystem中)。< / p>

问题: 如果我编辑了一个已经在数据库上获得图像的文档(所以如果$oldImage = $this->getImageByDocumentId($docId)实际返回$oldImage),一切正常。但是如果Document没有任何$oldImage,那么就会出现问题而它不会在数据库中插入新的图像(但它会将其保存在文件系统上!) < / p>

这是我的MySQLDocumentService

的一部分
public function editDocument($document) {

    try {
        $conn = $this->getAdapter();
        $conn->beginTransaction();

        $sql = "UPDATE Documents d
                SET d.name=:name, d.description=:description, d.content_id=:contentId, d.category_id=:categoryId, d.sharer_id=:sharerId, d.rating_id=:ratingId, d.price=:price
                WHERE d.document_id=:id";

        $prepStatement = $conn->prepare($sql);
        $prepStatement->execute(array(':id' => $document->getId(),
                                      ':name' => $document->getName(),
                                      ':description' => $document->getDescription(),
                                      ':contentId' => rand(1,2000),
                                      ':categoryId' => $document->getCategory()->getId(),
                                      ':sharerId' => 1,
                                      ':ratingId' => 1,
                                      ':price' => $document->getPrice()));


        // If image has been changed, take the old image name
        if (!is_null($document->getImage())) {
            $image = $document->getImage();
            $docId = $document->getId();
            $oldImage = $this->getImageByDocumentId($docId); // Here's the problem: if it doesn't find the oldImage, it doesn't insert the new one

            if (!is_null($oldImage)) {
                // If previous method succeeded, delete oldImage from DB and filesystem

                $oldImageName = $oldImage->getName();
                $this->deleteImageByName($oldImageName);
            }


            // Save new image (chosen on form) on db and filesystem

            if (file_exists("uploads/img/" . $image->getName())) {
                echo $image->getName() . " already exists. ";
                return false;

            } else {
                move_uploaded_file($image->getTempName(), "uploads/img/" . $image->getName());
            }


            // Saves image path on DB
            // If I edit a Document which has already got an image on the DB, everything works. But if the Document hasn't got any oldImage, something goes wrong and it doesn't insert the new Image on DB (but It saves it on filesystem!)

            $sqlImage = 'INSERT INTO Images (name, alt_name, position, description, type, size, document_id)
                       VALUES ("name", "altName", 2, "description", "type", "size", 2)';
            $prepStatementImg = $conn->prepare($sqlImage);

            $prepStatementImg->execute();
        }


        $conn->commit();

        return true;


    } catch (Exception $e) {
      $conn->rollBack();
      echo "Failed: " . $e->getMessage();
    }
}


public function getImageByDocumentId($docId) {
    try {
        $conn = $this->getAdapter();
        $conn->beginTransaction();

        $sql = 'SELECT i.image_id, i.document_id, i.name, i.alt_name, i.position, i.description, i.type, i.size
                FROM Images i
                WHERE i.document_id=:id';
        $prepStatement = $conn->prepare($sql);
        $prepStatement->execute(array(':id' => $docId));

        $result = $prepStatement->fetch();

        if ($result) {
            $image = new Image();
            $image->setName($result['name']);
            $image->setId($result['image_id']);
            $image->setAltName($result['alt_name']);
            $image->setDescription($result['description']);
            $image->setPosition($result['position']);
            $image->setType($result['type']);
            $image->setSize($result['size']);
            // Manca la costruzione del relativo documento, ma non penso serva
            $conn->commit();

            return $image;
        } else {
            return null;
        }

    } catch (Exception $e) {
      $conn->rollBack();
      echo "Failed: " . $e->getMessage();
    }
}

public function deleteImageByName($imgName) {

    try {
        $imgName = str_replace( array( '..', '/', '\\', ':' ), '', $imgName );
        unlink( "uploads/img/" . $imgName );

    } catch (Exception $fsEx) {
        echo "Failed: " . $fsEx->getMessage();
    }

    try {
        $conn = $this->getAdapter();
        $conn->beginTransaction();

        $sql = 'DELETE FROM Images
                WHERE name=:name';
        $prepStatement = $conn->prepare($sql);
        $prepStatement->execute(array(':name' => $imgName));

        $conn->commit();

        return true;


    } catch (Exception $e) {
      $conn->rollBack();
      echo "Failed: " . $e->getMessage();
    }
}

如果我发表评论$oldImage = $this->getImageByDocumentId($docId),它会在数据库上提交新图像的INSERT,一切正常。

我认为它可能是嵌套事务的一个问题,但它很奇怪,因为在db上正确找到$oldImage时一切正常。 (我还创建了一个扩展PDO类的类,如this guide上所述)。

我该怎么办?


编辑:在下面的一种答案中(通过Soyale),对嵌套方法和多次转换产生了怀疑。我因此粘贴我的MyPDO类,这应该避免多次转换(我希望至少如此)。这是由bitluni对PDO::beginTransaction manual page的评论。

class MyPDO extends PDO {

    protected $transactionCounter = 0;
    function beginTransaction()
    {
        if(!$this->transactionCounter++)
            return parent::beginTransaction();
       return $this->transactionCounter >= 0;
    }

    function commit()
    {
       if(!--$this->transactionCounter)
           return parent::commit();
       return $this->transactionCounter >= 0;
    }

    function rollback()
    {
        if($this->transactionCounter >= 0)
        {
            $this->transactionCounter = 0;
            return parent::rollback();
        }
        $this->transactionCounter = 0;
        return false;
    }
}

正如Soyale所说,

  

parent::openTransaction [我认为这是beginTransaction()的错误也不是一个好主意。或者,如果您有一些交易已经打开的旗帜,它可以通过考试。

我认为transactionCounter可能是你所谈论的旗帜。在我看来,这将让我正确地提交和回滚。我错了吗?

3 个答案:

答案 0 :(得分:1)

在我看来你在这个方法中犯了错误:getImageByDocumentId。 你没有在这个片段中提交交易:


if ($result) {
            $image = new Image();
            $image->setName($result['name']);
            $image->setId($result['image_id']);
            $image->setAltName($result['alt_name']);
            $image->setDescription($result['description']);
            $image->setPosition($result['position']);
            $image->setType($result['type']);
            $image->setSize($result['size']);
            // Manca la costruzione del relativo documento, ma non penso serva
            $conn->commit();

            return $image;
        } else {
            $conn->commit(); //Add this line :)
            return null;
        }

我想知道为什么那么多交易呢?它应该在一个事务中,所以如果一个查询失败,那么你可以回滚所有这些。

关于交易的更多信息:

让我们看看你的代码:

public function editDocument($document) {
        $conn = $this->getAdapter();
        $conn->beginTransaction(); // 1-st open transaction

        $this->getImageByDocumentId(...); // 2-nd opened transaction
        $this->deleteImageByName(...); // And the third one
}

public function getImageByDocumentId($docId) {
        $conn = $this->getAdapter();
        $conn->beginTransaction();
        //This method mainly select some data from DB so do you need transaction here ?
        //Query in this method does not affect any data
        //Data remains unchanged
        //So you can use sth like this
        $conn = $this->getAdapter();
        //$conn->beginTransaction(); //-> tyhis line is useless

    $sql = 'SELECT i.image_id, i.document_id, i.name, i.alt_name, i.position, 
            i.description, i.type, i.size
            FROM Images i
            WHERE i.document_id=:id';
    $prepStatement = $conn->prepare($sql);
    $prepStatement->execute(array(':id' => $docId));

    $result = $prepStatement->fetch();
    //(...) rest of code
}

public function deleteImageByName($imgName) {
        $conn = $this->getAdapter();
        $conn->beginTransaction();
}

因为你可以看到你的每一个方法都包含beginTransaction()它有点乱,导致嵌套的事务和提交。我主要使用Firebird DB,如果新事务打开,旧事务就会向下移动(我们收到新的资源处理程序)。

最有趣的是deleteImageByName()方法。如您所见,已经打开了一个事务(来自editDocument())。现在你打开第二个。现在您删除了您的图像deleteImageByName()已返回true并提交事务。


public function deleteImageByName($imgName) {
    //In my opinion this fragment should go after successfully deleted from database
    //and insert new image (prevent data loss)
    try {
        $imgName = str_replace( array( '..', '/', '\\', ':' ), '', $imgName );
        unlink( "uploads/img/" . $imgName );

    } catch (Exception $fsEx) {
        echo "Failed: " . $fsEx->getMessage();
    }

    //here you are deleting db record
    try {
        $conn = $this->getAdapter();
        $conn->beginTransaction();

        $sql = 'DELETE FROM Images
                WHERE name=:name';
        $prepStatement = $conn->prepare($sql);
        $prepStatement->execute(array(':name' => $imgName));

        //And you are commiting this (bad idea if there is more than only delete task)
        $conn->commit();

        return true;


    } catch (Exception $e) {
      $conn->rollBack();
      echo "Failed: " . $e->getMessage();
    }
}

现在如果由于某种原因插入失败,那么你没有新图像也没有旧图像。 如果只有一个事务(在主方法editDocument()中),您可以回滚事务并且不删除旧图像。 parent :: openTransaction也不是个好主意。或者,如果你有一个标志,其中一个交易已经打开,那么它可以通过考试。

通常,您应该为一项任务打开事务。让我们假设您的任务是:editDocumenteditDocument不是简单的行动。它由许多其他操作组成,因此事务(主方法中只有一个事务)应包含所有这些操作。 (在您的情况下删除旧图像并插入新图像)。像这样的东西:


public function editDocument() {
    $conn = $this->getAdapter();
    $conn->beginTransaction();

//1.    $this->deleteOldImage();
//2.    $this->insertNewOne();
//3.    $this->deleteFileWithOldImage();

    //Of every method should consist fail statement: $conn->rollback(); and throw exception

    $conn->commit();
}

对不起我的英文:)

修改 - &gt;为什么你的班级不太好

@KurtBourbaki这个扩展看起来不错,但事实并非如此。请注意,如果您忘记提交已打开的交易,那么您继续乱七八糟。在你的问题中有一个错误。缺少线。请尝试使用您的班级与该错误。这个怎么运作 ?让我们分析一下:


class MyPDO extends PDO {

    protected $transactionCounter = 0;

    //1. Increment counter regardless of whether it was set
    //2. PDO::beginTransaction() only if counter was 0
    function beginTransaction()
    {
        if(!$this->transactionCounter++)
            return parent::beginTransaction();
       return $this->transactionCounter >= 0;
    }

    //This is interesting
    //1. decrement counter
    //2. PDO::commit() but only when decrement counter == 0
    //So there is a core place because even with that class your primary bug will occur
    //because You have omitted exactly this one command.
    function commit()
    {
       if(!--$this->transactionCounter)
           return parent::commit();
       return $this->transactionCounter >= 0;
    }

    //rollback transaction looks good
}

我不知道为什么这个答案在php.net上投票如此之高。 我看到那里有更好的解决方案。有一个简单的私有布尔标志的解决方案由drm在http://pl1.php.net/manual/en/pdo.begintransaction.php上的melp dot nl发布。 我更喜欢这个,因为它确实不允许打开多个事务。

修改

Kurt指出我的选择也不好。 正如我在上一篇评论中写的那样。我首选的解决方案是永远不要打开嵌套事务。有关DBMS文档中的事务的一些信息。最受欢迎的数据库MySQL

答案 1 :(得分:0)

尝试在getImageByDocumentId函数中替换

  if($result)

通过

  if($prepStatement->rowCount()>0)

我认为你在执行$ prepStatement时会进入if,无论它是否有结果,所以你的图像是空的,导致你的脚本中断

答案 2 :(得分:0)

如果您在查询中直接插入数据,则必须有$ pdo-&gt;查询而不是$ pdo-&gt;准备好,这就是我的想法 我相信你的查询应该是     //在DB上保存图像路径      //如果我编辑一个已在DB上有图像的文档,一切正常。但是如果Document没有任何oldImage,那么就会出现问题而且它没有在DB上插入新的Image(但它将它保存在文件系统上!)

        $sqlImage = 'INSERT INTO Images (name, alt_name, position, description, type, size, document_id)
                   VALUES (:name, :altName, :position, :description, :type, :size, :id)';
        $prepStatementImg = $conn->prepare($sqlImage);