DB検索結果をキャッシュしてみる

なぜ、こんなことをするのかなーって自分でも思う。


「スタティック変数アノテーションをインターフェースで定義する」は停止中で、勉強するためのきっかけがほしくて。



でも、DTOドメインモデル風に昇格(?)してあげようかなーと思ったときに必要になるんじゃないかなーとも思った。


勢いにのって作った。
エンタープライズ アプリケーションアーキテクチャパターン (Object Oriented SELECTION)を参考にして。



まずは、インターフェースから作った。

public interface Registry {

    public Object addValue(Class clazz, Object value);

    public List addValues(Class clazz, List list);

    public List select(Class clazz, CollectionSelector selector);

    public Object detect(Class clazz, CollectionDetector detector);

    public List getAllValues(Class clazz);

    public Object getValue(Class clazz, Object key);

    public void clear(Class clazz);

}

CollectionSelector、CollectionDetectorは、ストリームラインオブジェクトモデリング―パターンとビジネスルールによるUMLからちょっと拝借して、若干自分風にカスタマイズ。
これものせるほどのものではないから省略。


実装クラスはテストクラスもきちんと作ったけど、これも省略。
実装はこんな感じ

public class RegistryImpl implements Registry {

    private HashMap cache = new HashMap();

    public Object addValue(Class clazz, Object value) {
        return doAddValue(clazz, value);
    }

    public List addValues(Class clazz, List list) {
        List result = new ArrayList();
        for (Iterator it = list.iterator(); it.hasNext();) {
            result.add(doAddValue(clazz, it.next()));
        }
        return result;
    }

    private Object doAddValue(Class clazz, Object value) {
        if (value == null) {
            return null;
        }

        Map map = getCacheMap(clazz);
        List list = getCacheList(clazz);

        if (map.containsKey(value)) {
            return map.get(value);
        } else {
            map.put(value, value);
            list.add(value);
            return value;
        }
    }

    public List select(Class clazz, CollectionSelector selector) {
        return (List) selector.select(getCacheList(clazz));
    }

    public Object detect(Class clazz, CollectionDetector detector) {
        return detector.detect(getCacheList(clazz));
    }

    public List getAllValues(Class clazz) {
        return getCacheList(clazz);
    }

    public Object getValue(Class clazz, Object key) {
        return getCacheMap(clazz).get(key);
    }

    public void clear(Class clazz) {
        cache.remove(clazz);
    }

    private List getCacheList(Class clazz) {
        return (List) getCacheClass(clazz).get("list");
    }

    private Map getCacheMap(Class clazz) {
        return (Map) getCacheClass(clazz).get("map");
    }

    private Map getCacheClass(Class clazz) {
        if (!cache.containsKey(clazz)) {
            Map map = new HashMap();
            map.put("list", new ArrayList());
            map.put("map", new HashMap());
            cache.put(clazz, map);
        }
        return (Map) cache.get(clazz);
    }

}

後半のRegistryImpl#getCacheClass()があやしい雰囲気を出してる。
RegistryImpl#select()のときの順番を大切にしたし、RegistryImpl#getValue()のときは気前よくとってきたいなーと思ってこんな感じになってしまった。
順番を保証したMapってどっかにあるかなー。


キャッシュするタイミングはやっぱりInterceptorで定義するべきかなーと思って

public class MappingCacheInterceptor extends AbstractInterceptor {

    private DaoMetaDataFactory daoMetaDataFactory;

    private Registry registry;

    private List methodNames = new ArrayList();

    // -----------------------------------------------------------------------

    public MappingCacheInterceptor(DaoMetaDataFactory daoMetaDataFactory,
            Registry registry) {
        
        this.daoMetaDataFactory = daoMetaDataFactory;
        this.registry = registry;
    }

    public void addMethodName(String methodName) {
        methodNames.add(methodName);
    }

    // -----------------------------------------------------------------------

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object result = getValuesFromCache(invocation);
        if (result != null) {
            return result;
        }

        result = invocation.proceed();
        if (isTargetMethod(invocation.getMethod().getName())) {
            result = addValuesForCache(invocation, result);
        }

        return result;
    }

    public Object getValuesFromCache(MethodInvocation invocation) {
        Class paramClass = getParameterClass(invocation);
        Class returnClass = getReturnClass(invocation);
        Class beanClass = getBeanClass(invocation);
        if (beanClass.equals(paramClass) && beanClass.equals(returnClass)) {
            Object param = getParameter(invocation);
            return registry.getValue(beanClass, param);
        }
        return null;
    }

    private Object addValuesForCache(MethodInvocation invocation, Object result) {
        Class beanClass = getBeanClass(invocation);

        if (result instanceof List) {
            return registry.addValues(beanClass, (List) result);
        } else {
            return registry.addValue(beanClass, result);
        }
    }

    private Object getParameter(MethodInvocation invocation) {
        return invocation.getArguments()[0];
    }

    private Class getParameterClass(MethodInvocation invocation) {
        Class[] params = invocation.getMethod().getParameterTypes();
        if (params.length != 1) {
            return null;
        }
        return params[0];
    }

    private Class getReturnClass(MethodInvocation invocation) {
        return invocation.getMethod().getReturnType();
    }

    private Class getBeanClass(MethodInvocation invocation) {
        Class targetClass = getTargetClass(invocation);
        BeanDesc daoBeanDesc = BeanDescFactory.getBeanDesc(targetClass);
        Field beanField = daoBeanDesc.getField(DaoMetaData.BEAN);
        Class beanClass = (Class) FieldUtil.get(beanField, null);
        return beanClass;
    }

    private boolean isTargetMethod(String methodName) {
        for (Iterator it = methodNames.iterator(); it.hasNext();) {
            String targetMethodName = (String) it.next();
            if (methodName.startsWith(targetMethodName)) {
                return true;
            }
        }
        return false;
    }

}

こんな感じで作ってみた。


キャッシュするっていっても自分の考えではrequestスコープのみでのキャッシュをイメージしてるから、更新は特に意識しなくてもいいかなーって思ったけど、更新後、もう一度DBから取得しなおして表示する場合もあるなーと思って、更新した際にキャッシュに対しても何らかの処理をすることにした。



考えた結果、単純にクリアするだけにした。
キャッシュクリア用のInterceptorはこんな感じ

public class ClearCacheInterceptor extends AbstractInterceptor {

    private DaoMetaDataFactory daoMetaDataFactory;

    private Registry registry;

    private List methodNames = new ArrayList();

    // -----------------------------------------------------------------------

    public ClearCacheInterceptor(DaoMetaDataFactory daoMetaDataFactory,
            Registry registry) {
        
        this.daoMetaDataFactory = daoMetaDataFactory;
        this.registry = registry;
    }

    public void addMethodName(String methodName) {
        methodNames.add(methodName);
    }

    // -----------------------------------------------------------------------

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object result = invocation.proceed();
        if (isTargetMethod(invocation.getMethod().getName())) {
            clearCache(invocation);
        }

        return result;
    }

    private void clearCache(MethodInvocation invocation) {
        Class targetClass = getTargetClass(invocation);
        BeanDesc daoBeanDesc = BeanDescFactory.getBeanDesc(targetClass);
        Field beanField = daoBeanDesc.getField(DaoMetaData.BEAN);
        Class beanClass = (Class) FieldUtil.get(beanField, null);

        registry.clear(beanClass);
    }

    private boolean isTargetMethod(String methodName) {
        for (Iterator it = methodNames.iterator(); it.hasNext();) {
            String targetMethodName = (String) it.next();
            if (methodName.startsWith(targetMethodName)) {
                return true;
            }
        }
        return false;
    }

}

MappingCacheInterceptor、ClearCacheInterceptorの両方とも#addMethodName()ってメソッドを持ってるけど、これは、DICONファイルを書くときにpointcutをたくさん書くのは大変だなーと思って追加した。
Interceptor自体にどのメソッドが呼ばれたら動きだすかを登録してしまうという、荒業。
cache.diconはこんな感じ

<components namespace="cache">
    <include path="dao.dicon"/>
    <include path="aop.dicon"/>

    <component class="study.xxx.extention.cache.registry.RegistryImpl" instance="request" />

    <component name="mappingCacheInterceptor" class="study.xxx.extention.cache.interceptors.MappingCacheInterceptor">
        <aspect pointcut="getValuesFromCache">aop.traceInterceptor</aspect>
        <initMethod name="addMethodName"><arg>"find"</arg></initMethod>
    </component>

    <component name="clearCacheInterceptor" class="study.xxx.extention.cache.interceptors.ClearCacheInterceptor">
        <initMethod name="addMethodName"><arg>"insert"</arg></initMethod>
        <initMethod name="addMethodName"><arg>"delete"</arg></initMethod>
        <initMethod name="addMethodName"><arg>"update"</arg></initMethod>
    </component>

    <component name="interceptor" class="org.seasar.framework.aop.interceptors.InterceptorChain">
        <initMethod name="add"><arg>mappingCacheInterceptor</arg></initMethod>
        <initMethod name="add"><arg>clearCacheInterceptor</arg></initMethod>
    </component>

</components>

Interceptorを定義するときに適用するメソッド名も記述してる・・・


これで準備ができて、最後にalldao.diconにInterceptorを利用するように記述を追加。

<components>
    <include path="dao.dicon"/>
    <include path="aop.dicon"/>
    <include path="study/xxx/dicon/cache.dicon"/>
    
    <component class="study.xxx.party.dao.PartyDao">
        <aspect>aop.traceInterceptor</aspect>
        <aspect>cache.interceptor</aspect>
        <aspect>dao.interceptor</aspect>
    </component>

    <component class="study.xxx.work.dao.TimecardDao">
        <aspect>aop.traceInterceptor</aspect>
        <aspect>cache.interceptor</aspect>
        <aspect>dao.interceptor</aspect>
    </component>
</components>

もし、毎回pointcutを記述してたら、大変だと思った。
あまり調べずに作ったから、もうちょっとドキュメントを読むようにしよう。




むむむ、、、困った。。。


DTOからキャッシュの情報を取り出すときはどうしよう?
DIでRegistryImplを設定するのもあるけど・・・
DTOを気軽にnewできないのは、やっぱヤダな・・・



なんかSeasar2を利用するようになってからわがままになってきたような気がする。
気軽にnewしたいってみんな思うようになるのかなー。