PHPで画像をMySQLに保存する。

CMSなどで画像をアップロードする機能を実装する際にその画像をDBに保存すると、サーバーを冗長化する際やサムネールを作成する機能を実装する際にいろいろと便利になるので、そのやり方をご紹介します。

DBに保存するメリット

  1. サーバーを冗長化する際に、画像をファイルとして保存するとそれぞれのサーバー間でアップロードデータの同期やNASの導入が必要となったりするが、DBに保存してしまえばその必要がなくなる。
  2. サムネール画像を作成するなどの機能で、途中で仕様変更等によりサムネール画像のサイズを変更したい場合などの、画像の出力フォーマットの変更などにも柔軟に対応できる。

DBに保存するデメリット

  1. DBに保存してそれを読み出すプログラムを経由する分、処理が多くなり必然的に負荷の増大につながる。
  2. DBのメンテナンス性の低下につながりやすい。
  3. レンタルサーバーなどで制限が出てくる。(たとえばMySQLのMAX_ALLOWED_PACKETなど)

DBに保存するときのコツ

上記のデメリットのうち付加が増大する点については、mod_rewriteを使用することで解決できます。

  1. 画像を出力する際にキャッシュとして出力結果を保存する。
  2. mod_rewriteでキャッシュファイルがある場合はPHPスクリプトにアクセスさせないで画像に直接アクセスするようにする。

DBスキーマサンプル

以下のようなテーブルを作成する。

画像のアップロードプログラムであらかじめ、mime_typeや画像の高さや幅を取得しておいてDBに保存しておくと、出力時になにかと都合がいいことが多いです。

画像のコンテンツは、m_contentに保存します。
base64でフォーマットするというような例も多いようですが、ここでは生で保存します。

CREATE TABLE `tbl_bin` (
 `m_id` varchar(32) NOT NULL,
 `m_content` longblob NOT NULL,
 `mime_type` varchar(100) NOT NULL,
 `width` smallint(5) unsigned NOT NULL,
 `height` smallint(5) unsigned NOT NULL,
 `m_modified` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
 PRIMARY KEY  (`m_id`),
 KEY `m_modified` (`m_modified`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

保存用PHPプログラム

以下はあくまでもサンプルです。
画像ファイルのコンテンツは以下のような感じでSQL文に挿入します。

$image = mysql_real_escpa_string(file_get_contents( $_FILE['image']['tmp_name'] ));

画像の幅や高さmime-typeは、getimagesize()で取得してください。

m_idには拡張子つきのファイル名を保存するのがベターです。
重複を避けるためにユーザーがつけたファイル名はさけて、自動的にファイル名を生成するようにしたほうがいいとおもいます。

出力用PHPプログラム

以下の例では、プログラム名はmedia.phpで、キャッシュを保存するディレクトリ名はmedia/であることを前提にしています。
これらを変更したい場合は、必要に応じて読み替えてください。

もっとも簡単な例では、以下のようなプログラムで画像の出力が可能です。
(あらかじめDBに接続する構文を記述してください。)

// URLからファイル名の部分を取得
$url= parse_url($_SERVER['REQUEST_URI']);
$id = basename($url['path']);

// DBから画像データを取得
$sql = "select m_content, mime_type from tbl_bin
  where m_id='".mysql_real_escape_string($id)."' limit 0,1";
$result = mysql_query($sql);

if (mysql_num_rows($result)) {
  $data = mysql_fetch_assoc($result);
    // キャッシュを保存
    $fp = fopen(dirname(__FILE__).'/media/'.$id, 'w');
    fwrite($fp, $data['m_content']);
    fclose($fp);
    // 画像を出力
    header('Content-type: '.$data['mime_type'].';');
    print $data['m_content'];
}else{
    header("HTTP/1.0 404 Not Found");
    print 'file not found';
}

.htaccessを設置

上述のPHPスクリプトで、アクセスがあった際に画像を保存するようにしたので、次はmod_rewriteでキャッシュがあればそのキャッシュを使用するようにする。

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule media/ media.php [L]

テスト

実際に画像(media.php)にアクセスしてみてキャッシュができるかどうかを確認したあと、media.phpをあえてリネームするなどして、同じURLで画像が再び表示されればOK。

以降は画像にアクセスがあってもDBへ接続は発生しません。

画像が増えすぎてディスク容量を圧迫するようなら、Cronで定期的に古い画像を削除するなどの処理を行うことで緩和されます。

MySQL用郵便番号データのダウンロード

密かに、Tracのほうでは以前から公開していたのですが、MySQL用の郵便番号データを配布します。

実は、このデータは前から作ってあったのですが、Cronで定期的にゆうびんホームページ郵便番号データダウンロードをチェックして更新されたらSVNリポジトリにコミットするというスクリプトを設置して、本日見事にデータの更新が検出できたので、ここでご紹介させていただくことにしました。

ダウンロード

MySQL用郵便番号データのダウンロード更新日はこちらで確認してください。

注意事項

このパッケージ内には、郵便番号データのMySQL用SQLファイル及びテーブルのスキーマが同梱されています。

データはCronで深夜に毎日チェックしています。
配布用のzipファイル内に含まれているデータは実際にMySQLに流し込んでテスト済みですが、SVNリポジトリにあるデータはそうとは限らないのでお気を付けください。

  • 読み仮名データの促音・拗音を小書きで表記するものを使用しています。
  • テーブル名はtbl_zipcodeです。
  • フィールド名は指定してありませんので任意のフィールド名でテーブルを作成してください。(15列あります。)
  • 半角カタカナは全角に変換してあります。
  • 文字コードはUTF-8です。
  • 各フィールドの詳細は 郵便番号データの説明の「郵便番号データファイルの形式等」をご参照ください。(列の順番はそのままです。)

SQLファイル内には郵便番号データすべてが含まれています。
次回以降のアップデートで流し込む際には、事前にデータの削除を行わないと重複して挿入されてしまいますのでご注意願います。

免責事項

本データを利用したことによる如何なる損害にも補償しかねますので、あらかじめご了承ください。

ライセンス

このデータに関しましては、著作権を主張しません。
ご自由に再配布していただいて結構です。

ですが、ご褒美を頂けるなら、とても感謝します。

PHP+MySQLでSQLインジェクション対策

ここで書いてあることだけでは十分とは言えませんが、これだけでも心当たりがあるシステムがやまほどあるはずです。

mysql_set_charsetを使用する

MySQLへクエリーを発行する際に文字エンコーディングを指定する方法として、以下のような方法がよく紹介されていますが、これは間違いです。

// 誤った方法
mysql_query("set names utf8");

以下のようにmysql_set_charset()関数を使用しなければ、mysql_real_escape_string()関数が誤った文字エンコーディングを前提にしてエスケープしようとしてしまいます。

// 正しい方法
mysql_set_charset("utf8");

詳しい説明は以下のサイトなどを参考にどうぞ。

mysql_real_escape_stringを使う

addslashes()というよく似た関数もありますが、それでは不完全です。

また、mysql_set_charset()と必ずセットで使用するべきです。

header()関数等で文字エンコーディングを明確に指定する。

これは、MySQLを使っているかどうかに限らず、常に心がけるべきです。

header("Content-type: text/html; charset=UTF-8");

header()関数ではなく、php.iniやhttpd.confでもデフォルトのエンコーディングを指定できますので、可能な場合はそちらも使用した方がベターです。

mb_convert_encoding()にautoを使用しない

可能であれば、mb_convert_encoding()に指定する変換前のエンコーディングはautoにしないで明示的に指定しましょう。

header()関数やphp.iniなどで出力エンコーディングを明示的にクライアントに知らせてあげれば、まっとうなブラウザなら期待通りのエンコーディングで返してくれます。

また、以下のような記述を入れて不正な文字エンコーディングを検出したらその後の処理を中断するべきです。

function check_encoding($key, $value) {
  if (!mb_check_encoding($value, 'UTF-8')) {
    die('不正な文字コード');
  }
}

array_walk_recursive($array, 'check_encoding');

内部エンコーディングにSJISを使用しない

これは論外。
某有名SNSのPHP関連のコミュニティで、SJISを使うという回答がかなり多くてびびりました。

PHPのマニュアルにも以下のような記載があります。

注意: SJIS, BIG5, CP936, CP949, GB18030 は、読者がパーサ/コンパイラ、 文字エンコーディングと文字エンコーディングの問題点について精通していない限り 内部エンコーディングとして使用するべきではありません。

検証していませんが、mb_real_escape_string()も期待通り動くかどうか怪しいですし、EUC-JPやUTF-8を使用すれば、そんな検証をする必要もありません。

てっとりばやく使えるPHPだからこそマニュアルには従うべきですよね。

その他

できれば、プリペアードSQLを使うのがベターなのですが。。。
まだ怪しいかんじなのでMySQLに関しては使ってないです。

ところで。。。

テーマファイルを変えてみました。