ログ日記

作業ログと日記とメモ

画像もDBに格納して管理する - Wicket編

PHPからJavaに移ると、アップロードされたファイルの格納場所に戸惑う。
画像をアップした場合、PHPならindex.phpと同じ階層にuploadImageとかいうディレクトリを作ってそこに置けばいい話なんだが…。


大抵はコンテキストルートの外にディレクトリを作ってそこに格納するっていう方針が普通?
でもzip等のダウンロード数をカウントするような処理が入るならともかく、ただの画像はwebappの中に置きたい。


はてブにタイムリーな話題が。

  • ブログにせよECサイトにせよ、およそWebサイトを構成しているデータはテキストだけでなく画像もある。
  • しかしデータベースに格納するのはテキストデータだけで、画像データまでDBに入れるということは考えられていない場合がほとんど。
  • でもよくよく考えると、データはデータ層=DB=に格納する、という一元管理の基本にのっとったほうがやっぱりいいんじゃないだろうか?
  • 容量食いすぎ?いや、Youtubeみたいなサイトならともかく、そもそも普通のWebサイトに載ってる画像なんて容量小さいし、最近ストレージ安いし。
  • ラージオブジェクト型は扱いづらい?たしかに。でもだったら画像をbase64エンコードしてテキスト化して文字列型カラムにつっこんどくのもいいのでは。
  • DB層やアプリ層に負荷がかかる?そこはmod_rewriteでキャッシュもどき作戦をとればいい。
http://neta.ywcafe.net/000774.html

と思ったら何年も前の記事なのね。



画像をDBに突っ込むのはいいとして、毎回問い合わせたら大変だよね…と思っていたところに良い案が。
ただPostgreSQLS2JDBCなら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ディレクトリ以下のファイル更新を忘れずに行えば完成。