【问题标题】:Best practice database transactions and storing files onto filesystem in PHP最佳实践数据库事务和将文件存储到 PHP 中的文件系统
【发布时间】:2012-07-24 08:48:13
【问题描述】:

如果用户将用户数据与文件一起上传,用户数据存储在数据库中,并且文件存储在文件系统中,那么关于完整性的最佳做法是什么?

目前我会使用 PHP 和 PDO 执行以下代码 sn-p 之类的操作(代码未经测试,但我希望你能明白我的意思)。我不喜欢 User::insert 方法中的保存图像部分。有什么好办法吗?

<?php
User::insert($name, $image, $ext);

Class User{
    static public function insert($name, $image, $ext){
        $conn = DB_config::get();

        $conn->beginTransaction();

        $sth = $conn->prepare("
                                INSERT INTO users (name)
                                values(:name)
                                ;");

        $sth->execute(array(
                                ":name"     =>  $name
                                ));

        if ($conn->lastInsertId() > -1 && Image::saveImage($image, IMAGE_PATH . $conn->lastInsertId(). $ext))
            $conn->commit();
        else
            $conn->rollback();

        return $conn->lastInsertId();
    }
}

Class Image{
    static public function saveimage($image, $filename){
        $ext = self::getExtensionFromFilename($filename);

        switch($ext){
            case "jpg":
            case "jpeg":
                return imagejpeg(imagecreatefromstring($image), $filename);
        }

        return false;
    }
?>

【问题讨论】:

  • 不确定您所说的完整性是什么意思。如果您的意思是验证文件是否是未损坏的图像,您可以使用 ImageValidator 类进行检查,如以下问题所述:stackoverflow.com/questions/1581136/…
  • 另外,您可能对 DAO 模式感兴趣:odi.ch/prog/design/php/guide.php
  • @JamesPoulson 完整我的意思是,如果出于某种原因无法将用户存储在数据库中,则不得存储图像,反之亦然。但也许我的问题表述错了,但我确实在寻找某种设计模式来分离用户和图像的东西。您的链接非常有帮助。谢谢
  • @cookie 我会做一些类似于下面建议的事情:检查代码中的图像,可能使用 uniqid(技术上唯一)或 md5 哈希(文件完整性,相对唯一)保存图像,将图像注册到专用表(解耦并允许 N 个图像),注册用户,如果用户注册被取消或用户被删除,则删除数据库和磁盘中的图像,如果用户注册成功,则使用用户的 PK/id 更新图像表。

标签: php transactions pdo


【解决方案1】:

试试这个。

  • 将图像保存到磁盘的工作区中。最好保存到 与最终目的地在同一卷上的工作区。 最好也放在单独的目录中。

  • 启动与数据库的事务。

  • 插入您的用户。

  • 在用户 ID 之后重命名图像文件。

  • 提交事务。

它的作用是首先执行风险最大的操作,即保存图像。各种各样的事情都可能发生在这里——系统可能会失败,磁盘可能会填满,连接可能会关闭。这(可能)是您的操作中最耗时的,所以它绝对是最危险的。

完成后,您开始事务并插入用户。

如果此时系统出现故障,你的插入会被回滚,图像会在临时目录下。但是对于您的真实系统,实际上“什么都没有发生”。可以使用自动功能清理临时目录(即重新启动时清理、清理超过 X 小时/天的所有内容等)。文件在此目录中的时间跨度应该很短。

接下来,将图像重命名为其最终位置。文件重命名是原子的。它们工作或不工作。

如果系统在此之后,用户行将被回滚,但文件将在其最终目的地。但是,如果重新启动后有人尝试添加一个新用户,而该用户恰好与失败的用户具有相同的用户 ID,则他们上传的图像将简单地覆盖现有的图像 - 没有害处,没有犯规。如果用户 id 不能重复使用,您将拥有一个孤立的图像。但这可以通过自动化程序每周或每月一次合理地清理。

最后提交事务。

此时一切都在正确的位置。

【讨论】:

  • 如果最后一个动作——提交失败了怎么办?
  • @StarCub 就像我在最后提到的那样。用户行将被回滚,文件将位于其最终目的地并成为孤立文件。
【解决方案2】:

这似乎是使用 try/catch 块来控制流程执行的最佳时机。您似乎还遗漏了大部分难题,即在用户表中保存图像保存期间创建的图像路径给用户。

以下代码未经测试,但应该让您走上正轨:

Class User{

    static public function insert($name, $image, $ext)
    {
        $conn = DB_config::get();

        // This will force any PDO errors to throw an exception, so our following t/c block will work as expected
        // Note: This should be done in the DB_config::get method so all api calls to get will benefit from this attribute
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        try {

            $conn->beginTransaction();

            $sth = $conn->prepare("
                INSERT INTO users (name)
                values(:name);"
            );

            $sth->execute(array(":name" => $name));

            $imagePath = Image::saveImage($image, IMAGE_PATH . $conn->lastInsertId(). $ext));

            // Image path is an key component of saving a user, so if not saved lets throw an exception so we don't commit the transaction
            if (false === $imagePath) {
                throw new Exception(sprintf('Invalid $imagePath: %s', $imagePath));
            }

            $sth = $conn->prepare("UPDATE users SET image_path = :imagePath WHERE id = :userId LIMIT 1");

            $sth->bindValue(':imagePath', $imagePath, PDO::PARAM_STR);
            $sth->bindValue(':userId', $conn->lastInsertId(), PDO::PARAM_INT);

            $sth->execute();

            // If we made this far and no exception has been thrown, we can commit our transaction
            $conn->commit();

            return $conn->lastInsertId();

        } catch (Exception $e) {

            error_log(sprintf('Error saving user: %s', $e->getMessage()));

            $conn->rollback();
        }

        return 0;
    }
}

【讨论】:

  • 我认为不需要更新来保存图像路径。例如,如果添加了用户并且 ID 为“24”,则图像将为 path/24.jpg。
  • 对不起,需要存储路径,在我的应用程序中我存储了扩展名。
  • @cooxie:我明白了。我添加了该更新语句,让您了解如果您需要存储将用户链接到数据库中的图像所需的数据,try/catch 将如何帮助流程执行。随意更改它以满足您的要求。
【解决方案3】:

如果您将 Image 和 User 类更改为隐含接口,您可以执行这样的类...

class Upload {

    public static function performUpload($name, $image, $ext) {

        $user = new User($name);
        $user->save();

        $img = new Image($image, $ext);
        $img->save();

        $isValid = $user->isValid() && $image->isValid();
        if (!$isValid) {

            $user->delete();
            $img->delete();
        }

        return $isValid;
    }
}

【讨论】:

  • 我喜欢这种方法,但是当两者之一无效或有效但写入数据库或文件系统失败时,您将如何撤消保存的图像和保存的用户?
【解决方案4】:

我认为你应该使用command pattern,首先调用文件操作,然后调用数据库操作。因此,您可以使用数据库的事务回滚并为文件操作编写手动回滚,例如您可以将文件的内容存储在内存中或临时存储中以防万一发生故障......这更容易回滚文件然后手动回滚数据库记录...

哦,除非您想要死锁,否则锁定资源始终以相同的顺序...例如,始终以 ABC 顺序锁定文件,并始终在文件操作之后使用数据库。顺便说一句,在极少数情况下,您可以使用文件系统事务。这取决于您服务器的文件系统...

【讨论】:

    猜你喜欢
    • 2010-09-24
    • 2011-09-04
    • 2011-08-29
    • 2010-09-22
    • 2014-01-13
    • 2011-10-19
    • 2011-12-19
    • 2011-05-10
    • 1970-01-01
    相关资源
    最近更新 更多