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 を持つ新しいユーザーを誰かが追加しようとすると、アップロードされた画像は単に既存の画像を上書きします。害も問題もありません。ユーザー ID を再利用できない場合は、孤立したイメージになります。ただし、これは、自動化されたルーチンを使用して、週に 1 回または月に 1 回合理的にクリーンアップできます。

最後にトランザクションをコミットします。

この時点で、すべてが適切な場所にあります。

于 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 に答える