画像もDBに格納して管理する - Wicket編
PHPからJavaに移ると、アップロードされたファイルの格納場所に戸惑う。
画像をアップした場合、PHPならindex.phpと同じ階層にuploadImageとかいうディレクトリを作ってそこに置けばいい話なんだが…。
大抵はコンテキストルートの外にディレクトリを作ってそこに格納するっていう方針が普通?
でもzip等のダウンロード数をカウントするような処理が入るならともかく、ただの画像はwebappの中に置きたい。
http://neta.ywcafe.net/000774.html
- ブログにせよECサイトにせよ、およそWebサイトを構成しているデータはテキストだけでなく画像もある。
- しかしデータベースに格納するのはテキストデータだけで、画像データまでDBに入れるということは考えられていない場合がほとんど。
- でもよくよく考えると、データはデータ層=DB=に格納する、という一元管理の基本にのっとったほうがやっぱりいいんじゃないだろうか?
- 容量食いすぎ?いや、Youtubeみたいなサイトならともかく、そもそも普通のWebサイトに載ってる画像なんて容量小さいし、最近ストレージ安いし。
- ラージオブジェクト型は扱いづらい?たしかに。でもだったら画像をbase64エンコードしてテキスト化して文字列型カラムにつっこんどくのもいいのでは。
- DB層やアプリ層に負荷がかかる?そこはmod_rewriteでキャッシュもどき作戦をとればいい。
と思ったら何年も前の記事なのね。
画像をDBに突っ込むのはいいとして、毎回問い合わせたら大変だよね…と思っていたところに良い案が。
ただPostgreSQLとS2JDBCならbytea型が簡単に扱えるのでbase64エンコードはしない方針で。
あとサーブレット+mod_rewriteも混乱しそうなのでmod_rewriteも使わずに。
キャッシュとDB問い合わせの切り替えはWicketのRequestMapperで実装する。
WebApplicationのinit
public class WebApp extends AuthenticatedWebApplication { @Override protected void init(){ super.init(); ... mount(new DynamicImageMapper("/cache/${id}"));
cacheディレクトリ以下に画像を生成することにする。
DynamicImageMapper.java
/** * Copy from {@link org.apache.wicket.request.mapper.ResourceMapper} * @see DynamicImageRequestHandler#detach(org.apache.wicket.request.IRequestCycle) * @author nishimura * */ public class DynamicImageMapper extends AbstractMapper { // encode page parameters into url + decode page parameters from url private final IPageParametersEncoder parametersEncoder = new PageParametersEncoder(); // mount path (= segments) the resource is bound to private final String[] mountSegments; public DynamicImageMapper(String path){ Args.notEmpty(path, "path"); mountSegments = getMountSegments(path); } @Override public IRequestHandler mapRequest(Request request) { final Url url = request.getUrl(); // check if url matches mount path if (urlStartsWith(url, mountSegments) == false) return null; // now extract the page parameters from the request url PageParameters parameters = extractPageParameters(request, mountSegments.length, parametersEncoder); // check if there are placeholders in mount segments for (int index = 0; index < mountSegments.length; ++index){ String placeholder = getPlaceholder(mountSegments[index]); if (placeholder != null){ // extract the parameter from URL if (parameters == null){ parameters = new PageParameters(); } parameters.add(placeholder, url.getSegments().get(index)); } } String arg = null; try { arg = parameters.get("id").toString(); }catch (StringValueConversionException e){ return null; } final String fileName = arg; if (ImageFileUtil.existsCacheFile(fileName)) // (※1) return null; IResource res = new DynamicImageResource(){ private static final long serialVersionUID = 1L; @Override protected byte[] getImageData(Attributes attributes) { try { String hash = ImageFileUtil.getHash(fileName); ImageService service = SingletonS2Container.getComponent(ImageService.class); Image image = service.findById(hash); if (image == null) return null; ImageFileUtil.writeCacheFile(fileName, image.imageData); // (※2) return image.imageData; }catch (IOException e){ return null; } } }; return new DynamicImageRequestHandler(res, parameters); } @Override public int getCompatibilityScore(Request request) { return 0; } @Override public Url mapHandler(IRequestHandler requestHandler) { if (!(requestHandler instanceof DynamicImageRequestHandler)) return null; ResourceRequestHandler handler = (ResourceRequestHandler) requestHandler; Url url = new Url(); // add mount path segments for (String segment : mountSegments){ url.getSegments().add(segment); } // replace placeholder parameters PageParameters parameters = new PageParameters(handler.getPageParameters()); for (int index = 0; index < mountSegments.length; ++index){ String placeholder = getPlaceholder(mountSegments[index]); if (placeholder != null){ url.getSegments().set(index, parameters.get(placeholder).toString("")); parameters.remove(placeholder); } } // create url return encodePageParameters(url, parameters, parametersEncoder); } }
色々やっているが、ほとんどResourceMapperと同じ。うまいこと継承・拡張する方法が思い付かなかったので取り敢えず一通りコピーしてから編集した。
DynamicImageRequestHandlerはResourceRequestHandlerを継承した中身無しのクラス。instanceofで使うためだけにクラスを分けた。ここから色々拡張していくっていうわけでもないなら、分ける必要は無いかも。
ImageFileUtilクラスはWebApplication.get().getServletContext().getRealPath("cache") のパスにファイルを書いたり読んだりMD5を計算したり。だいたい名前の通り。
(※1)で、cacheディレクトリにファイルがあればnullを返却し、このMapperでは何も処理しない。Wicketは実際のファイルが存在するパスでアクセスしたらそのまま表示するので、ここではnullを返すだけで良い。
(※2)では、DBのimageテーブルからImageエンティティを取得し、PostgreSQL上でbytea型のカラムはJavaのbyte[]型にマッピングされているのでそのままcacheディレクトリに書き込みつつ返却する。
あとはImageService#update時(やImageService#delete時)にcacheディレクトリ以下のファイル更新を忘れずに行えば完成。