MySQLと画像ファイルのトランザクション問題を考える
画像データを外部ファイルで持つのと、データベースに格納しておくのとでは、どちらが良いのでしょうか? なかなか難しい問題です。
今回は画像ファイルを外部に持つことのメリット・デメリットを考え、落とし所を探っていくことにしましょう。
メリットとデメリット
メリット
- 画像がすぐに参照できるので扱いやすい
- 直接URL参照できる
- データベースと別のところにも置いておける(CDNに便利)
デメリット
- データベースと連動して画像を削除した場合、データベースレコードの削除に失敗しても画像ファイルを元に戻せない(トランザクションの問題)
- サーバー移転時にデータ移行する場合、データベース移行だけでは済まない(面倒)
特に、トランザクションの問題は深刻です。 データベース上のデータと、画像ファイルの関係性が崩れてしまう可能性があるためです。
画像削除にもトランザクションを実現してみる
この問題を回避する方法をいろいろ考えた結果、以下のステップでトランザクションぽいことが出来るのではないかと思いつきました。
データベース・トランザクションと画像ファイル削除のステップ
- 1.画像を/tmp ディレクトリに移動させる。
- 2.データベーストランザクション開始(BEGIN)
- 3.データベース削除(DELETE ~)
- 4.結果の確認
- 5.エラーであれば、/tmp の画像を元に戻してロールバック(ROLLBACK)。
- 6.OKであれば、/tmp の画像を削除してコミット(COMMIT)。
Linux上で/tmp ディレクトリの内容は定期的にクリアされるので、6のステップで失敗してもいずれ削除されることになります。
これをPHPで実現したのが以下のコードです。
やや実用的にするために、ユーザーごとの画像フォルダを削除するという想定で書いてみました。 基本的な命令しか使っていないのでPHP4, PHP5両方で動作すると思います。
稚拙なコードですが、参考にしてみてください。
サンプルコード
/** * ディレクトリの削除関数 * ファイルが中にあった場合もすべて削除します。 * 削除対象のディレクトリの中はすべてファイルでなければなりません。 * * @param string $dir 削除対象のディレクトリ * @return boolean 成功した場合trueを返します */ function removeDir($dir) { if (is_dir($dir) && !is_link($dir)) { foreach(glob($dir.'/*') as $sf) { if (!removeDir($sf) ) return false; } return rmdir($dir); } else { return unlink($dir); } } /** * 画像フォルダなどの個別データディレクトリがある場合は * 一旦/tmp ディレクトリにリネームして移動させ、成功したら削除します。 * 失敗したら退避先のディレクトリから元に戻します。 */ /** * MySQLへの接続 */ if (!($cn = mysql_connect("localhost", "myUser", "myPassword"))) return false; if (!(mysql_select_db("myDB"))) return false; /** * 削除したいディレクトリ */ $dataDir = "/path/to/mydataDir_001"; /** * 一時退避時のディレクトリ */ $tmpDataDir = "/tmp/mydataDir_001"; if (is_dir($dataDir) and !rename($dataDir, $tmpDataDir)) return false; mysql_query("BEGIN"); if (mysql_query("DELETE FROM mytable WHERE myid = '001'") == false) { /** * 失敗 (フォルダを戻してロールバック) */ if (is_dir($tmpDataDir)) rename($tmpDataDir, $dataDir); mysql_query("ROLLBACK"); return false; } else { /** * 成功 (フォルダを削除してコミット) */ if (is_dir($tmpDataDir)) removeDir($tmpDataDir); mysql_query("COMMIT"); return true; }
2010-03-12