【问题标题】:Database insert isn't executed in nested transaction数据库插入不在嵌套事务中执行
【发布时间】:2014-03-19 15:59:18
【问题描述】:

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

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

这是我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),它会在 DB 上提交新图像的 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;
    }
}

正如索亚尔所说,

parent::openTransaction [我认为这是 beginTransaction() 的拼写错误] 也不是一个好主意。或者,如果你有一些标志表明其中一项交易已经打开,它可以通过考试。

我认为transactionCounter 可能是您所说的标志。在我看来,这会让我正确地提交和回滚。我错了吗?

【问题讨论】:

  • 你的问题我不清楚,但听起来你必须更新图像而不是插入它
  • 我必须删除旧图像并插入新图像 :)
  • 为什么要插入:INSERT INTO Images (name, alt_name, position, description, type, size, document_id) VALUES ("name", "altName", 2, "description", "type" , "size", 2) 完全没有变量。
  • @CodeBird 是因为我想缩短代码。实际版本更长。但这不是问题,因为当找到 $oldImage 时插入工作完美无缺。
  • @KurtBourbaki 我已经编辑了我的答案并分析了为什么您的解决方案不太好

标签: php pdo transactions


【解决方案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();
    }
}

现在,如果由于某种原因插入失败,那么您既没有新图像也没有旧图像。 如果只有一个事务(在 main 方法 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();
}

对不起我的英语:)

编辑: -> 为什么你的课不太好

@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

【讨论】:

  • 这就是答案!似乎现在一切正常。但是,当您说“许多交易”时,您是什么意思?我在editDocument() 方法中只看到一笔交易。所有其他事务都在嵌套方法中,但我使用扩展 PDO 的类来管理它们,正如我在问题中链接的指南中所写:)
  • @KurtBourbaki 您正在为每个查询打开新交易(为什么?)。事务的主要目的是防止在其他查询失败时执行某些查询。换句话说,如果一个查询失败,不要在 PDO beginTransaction 中提交其他已经执行的(回滚事务)关闭自动提交。 Yoy 正在以各种方式开启新交易。
  • 我需要分别使用每个方法,所以每个方法应该独立工作。所以,它必须有自己的事务。我错了吗?如果一个方法嵌套在另一个方法中(就像我的问题中的情况),我调用parent::beginTransaction()。我误解了交易吗?
  • @KurtBourbaki 我已经编辑了我的答案:) 并尝试解释交易
  • drm at melp dot nl 在该网站上的回答中,当 commit() 完成时,他不使用任何 if。是不是错了?如果我在父方法上打开事务,它将在嵌套方法的提交时关闭。我认为这就是为什么其他指南使用计数器而不是布尔值的原因。你怎么看?
【解决方案2】:

如果您直接在查询中插入数据,您必须有一个 $pdo->query 而不是 $pdo->prepare,这就是我的想法 我相信您的查询应该是 // 将图像路径保存在数据库中 // 如果我编辑一个已经在数据库中获得图像的文档,一切正常。但是如果 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);

【讨论】:

  • 当我回复@CodeBird 时,在问题中我写了一个不带参数的较短查询,因为我想缩短代码。实际版本更长。但这不是问题,因为当找到 $oldImage 时插入工作完美:)
【解决方案3】:

尝试替换,在 getImageByDocumentId 函数中,

  if($result)

通过

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-26
    • 1970-01-01
    • 2016-02-15
    • 1970-01-01
    相关资源
    最近更新 更多