如果用户将用户数据与用户数据存储在数据库中并将文件存储到文件系统中的文件一起上传,那么有关完整性的最佳做法是什么?
目前我会使用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;
}
?>
答案 0 :(得分:5)
试试这个。
将图像保存到工作区中的磁盘。最好将它保存到 与最终目的地在同一卷上的工作区。 最好将它放在一个单独的目录中。
使用数据库启动事务。
插入您的用户。
在用户ID后重命名图像文件。
提交交易。
这样做首先执行风险最高的操作,即保存图像。这里可能发生各种各样的事情 - 系统可能会失败,磁盘可能会填满,连接可能会关闭。这可能是您操作中最耗时的,因此它绝对是最危险的。
完成此操作后,您将启动事务并插入用户。
如果系统此时失败,您的插入将被回滚,图像将在临时目录中。但对于你的真实系统,实际上“没有发生任何事情”。可以使用自动功能清理临时目录(即重新启动时清理,清除超过X小时/天的所有内容等)。文件在此目录中的时间跨度应该非常短。
接下来,将图像重命名为最终位置。文件重命名是原子的。他们工作或他们不工作。
如果系统在此之后,用户行将回滚,但文件将在其最终目的地。但是,如果在重新启动后有人试图添加一个恰好具有与失败者相同的用户ID的新用户,他们上传的图像将只覆盖现有用户 - 没有伤害,没有犯规。如果无法重复使用用户ID,您将拥有孤立的图像。但这可以合理地每周清理一次或通过自动化程序每月清理一次。
最后提交交易。
此时一切都在正确的位置。
答案 1 :(得分: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;
}
}
答案 2 :(得分: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;
}
}
答案 3 :(得分:0)
我认为你应该使用command pattern,然后在数据库操作之后调用文件操作。因此,您可以使用数据库的事务回滚并为文件操作编写手动回滚,例如,您可以将文件的内容存储在内存或临时存储中,以防出现故障...更容易回滚文件然后手动回滚数据库记录...
哦,除非你想要死锁,否则总是以相同的顺序锁定资源...例如,锁定文件始终按ABC顺序并始终在文件操作后使用数据库。顺便说一句,在极少数情况下,您可以使用文件系统事务。这取决于您的服务器的文件系统......