9

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

目前,我会使用 PHP 和 PDO 执行以下代码片段(代码未经测试,但我希望你能明白我的意思)。我不喜欢 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;
    }
?>
4

4 回答 4

7

试试这个。

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

  • 启动与数据库的事务。

  • 插入您的用户。

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

  • 提交事务。

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

完成此操作后,您将启动事务并插入用户。

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

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

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

最后提交事务。

在这一点上,一切都在正确的地方。

于 2012-07-25T20:51:59.910 回答
2

如果将 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;
    }
}
于 2012-07-25T20:33:57.270 回答
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;
    }
}
于 2012-07-25T20:50:53.127 回答
0

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

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

于 2014-04-09T01:14:52.390 回答