ローカルキャッシュ

MyBatisで、同一トランザクション内で同じシーケンスから値を2回取得する
ようなコードを書くと、同じ値が返されてしまう。サンプルはこんな感じ。

<select id="nextId" resultType="Long">
    select nextval( #{name} ) as value
</select>
SqlSession session = ・・・;

SequenceValue sequenceValue = new SequenceValue();
sequenceValue.setName("seq_testid");
Long result1 = (Long) session.selectOne("Test1.nextId", sequenceValue);
System.out.println(result1);

// 以下があるとうまくいくけど、SpringとMyBatisを連携するので、
// sessionオブジェクトを直でさわりたくはない。
// session.clearCache();

Long result2 = (Long) session.selectOne("Test1.nextId", sequenceValue);
System.out.println(result2);


で、このresult1とresult2が同じになるという罠。
そもそも、ログを見ると2回目の取得処理でSQLが発行されていない。


調べていくと、どうやらこれのようで。
http://code.google.com/p/mybatis/issues/detail?id=126


以下のようにflushCache="true"を付けると、ローカルキャッシュがクリア
されるようで、2回目の取得処理でもSQLが実行され違う値が取得できる。

<select id="nextId" resultType="Long" flushCache="true">
    select nextval( #{name} ) as value
</select>


別にシーケンスに限ったことではなく、通常のSELECTのSQLでも
同じ動作になるみたい。
そもそも、見えないところでキャッシュが効いているという
事実のほうが怖い・・・。他にもトラップがありそうだなぁ。

HandlerMappingを自動でやりたい

リクエストのパスに対応するControllerを見つけるためのクラスがHandlerMapping。
こういうのは、命名規約といったルール付けをしておくことで、実装者が
設定ファイルを書かなくても自動でできるようにしたい。


イメージとしては、/top/top.formにアクセスするとtop.TopControllerが
自動で呼ばれるようにしたい。


今考えている前提として、

という条件。


で、ControllerClassNameHandlerMappingを使うとTopControllerは
以下のURLにマッピングされると認識される。

  • top/top
  • top/top/*

つまりは、top/top.formには反応しないわけで、困った困った・・・
というのをどうにかしたいというのが今回のお話。


とりあえず思いつきで2つを試してみた。

  • ControllerClassNameHandlerMappingを拡張する
  • 独自にHandlerMappingを作る

ControllerClassNameHandlerMappingを拡張してしまう

ControllerClassNameHandlerMappingで、top.TopControllerが
/top/top*に対して登録するようにしてしまう。


コードを追う限り、ControllerClassNameHandlerMapping#generatePathMappings
でTopControllerがMultiActionControllerTypeと認識されるから悪いわけで、
そうでないと認識されれば/top/top*に対するマッピングが登録されるはず。


やるとすると以下の方法となる。

  • ControllerClassNameHandlerMappingを継承したクラスを作成し、isMultiActionControllerTypeメソッドをオーバーライドしちゃう。
  • isMultiActionControllerTypeで使っているControllerTypePredicateを置き換える。
    • →ただしこいつパッケージプライベートだったりする。


ただ、@Controllerを付けたクラスがMultiActionControllerTypeでなくなる
ことの弊害は不明・・・。

独自にHandlerMappingを作る

単にパス名とControllerをマッピングするHandlerMappingを自作してみる。
Spring全体の動作を把握しているわけじゃないので、あんまりやりたくないけど、
なんかちょっとやってみたかったというだけ。


top/top.formにアクセスした場合、top.formの部分からTopControllerが
マッピングされるようにする。テスト的に作ったのがこんな感じ。
# 時間がないのでガリガリ書いてるのはヒミツ
# 上半分はどっかのコードのパクりだしw

public class PathNameHandlerMapping extends AbstractUrlHandlerMapping {

    @Override
    protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
        Object handler = getHandlerMap().get(urlPath);
        if (handler != null) {
            if (handler instanceof String) {
                String handlerName = (String) handler;
                handler = getApplicationContext().getBean(handlerName);
            }
            validateHandler(handler, request);
            return buildPathExposingHandler(handler, urlPath, urlPath, null);
        }

        int index = urlPath.lastIndexOf('/');
        if (index == -1) {
            return null;
        }

        String path = urlPath.substring(index + 1);
        int extIndex = path.indexOf('.');
        if (extIndex != -1) {
            path = path.substring(0, extIndex);
        }

        String handlerName = path + "Controller";
        if (!getApplicationContext().containsBean(handlerName)) {
            return null;
        }

        handler = getApplicationContext().getBean(handlerName);
        validateHandler(handler, request);
        registerHandler(urlPath, handlerName);
        return buildPathExposingHandler(handler, path, path, null);
    }

}

なんとなく動いているようだけど、不正なパスに対してもControllerが動くわけで、
セキュリティ的に大問題な気がする。/aaa/xxx.formにアクセスできないユーザーが、
/xxx.formでアクセスできちゃったりしそーなので。
あと、これやるとパス名は必ず一意としなければならない(/aaa/xxx.formと
/bbb/xxx.formというのは作れない)わけで、それもよろしくない。




色々と悩んだけど、こんな解決策しか思いつかないんで、
もうちっとソースをあさってみるかぁ。
そもそも、1リクエスト=1Controllerとしてるのが悪な気がするけど。

動的SQLを試す

ユーザー情報的なテーブルがあって、ログインIDかユーザー名か(またはその両方)で
検索するような機能をMyBatisとPostgreSQLで試す。


で、まずはベタにこんな感じ。

  • 画面からくるパラメータの前後に%を付加してlike〜という変数に設定しておく。
  • <if>を使って、like〜という変数がnullでなければ、その条件を追加する。
    • test属性には、ognlライクな記述ができるみたい。
  • <where>で囲うと、1つ目の条件文(loginId)がない場合、2つ目以降のandを勝手に取ってくれる。
    • これ便利なんだけど、SQLを記述する際にandをまったく付けないで、自動的にandを付けてくれた方が楽だよなぁ。始めそういう風に書いてて動かなくてハマったというだけなんだけど。
<select id="searchUser" resultType="UserInfoVo">
    select * from userinfo
    <where>
        <if test="likeLoginId != null">
            loginid like #{likeLoginId}
        </if>
        <if test="likeUserName != null">
            and username like #{likeUserName}
        </if>
    </where>
</select>

で、これだとlike〜ってフィールドを増やさなきゃいけないし、%を付加する処理を入れるのもめんどい。
というわけで、こんな感じにしてみる(search〜というパラメータが画面からくる値まんま)。

<select id="searchUser" resultType="UserInfoVo">
    select * from userinfo
    <where>
        <if test="searchLoginId != null and searchLoginId != ''">
            loginid like '%' || #{searchLoginId} || '%'
        </if>
        <if test="searchUserName != null and searchUserName != ''">
            and username like '%' || #{searchUserName} || '%'
        </if>
    </where>
</select>


とりあえずこれでいいかぁ。
nullチェック、空文字チェックをシンプルにできないものか・・・。

プログラムでトランザクション制御

基本的にはアノテーショントランザクション制御するけど、
独自にコミットしたいなーって時もあるわけで、そんなテスト。

@Service
public class HogeServiceImpl implements HogeService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void execute() throws Exception {
        DefaultTransactionDefinition transactionDefinition 
            = new DefaultTransactionDefinition(TransactionDefinition.REQUIRED);
        TransactionStatus status = transactionManager.getTransaction(transactionDefinition);

        try {
            // なんか更新処理
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
        transactionManager.commit(status);

        status = transactionManager.getTransaction(transactionDefinition);
        try {
            // なんか更新処理
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
        transactionManager.commit(status);
    }
}

マニュアルまんまなわけですが、2個目がコケても1個目はちゃんと更新されている。
ログを見るとなんとなくできているようで。
前もあったけど、Spring-MyBatisで連携しているとトランザクション制御
されてなくても勝手にSQLが実行されちゃったりするので、ログをきちんと
見てないと怖い。


メソッド分けてアノテーション付ければいけるのかと思ったけど、
どうも動かなかった。まぁトランザクション制御したいがために
メソッド分けるのもどうかなーというのもあるし。


さて、このトランザクションをコミットも何もしなかった場合、
はたしてどうなるのかなぁ。
try-catchがきちんと実装できないなんてザラにあるし・・・。

Spring3.0とMyBatisの設定メモ

SpringとMyBatisを連携して使う場合、MyBatisが用意している連携用ライブラリを使うらしい。
というわけでメモ。正しいのかどうかは知らないけど、ちゃんとできてるようには見える。

  • 設定ファイル

データソースとMyBatisとトランザクションの定義。
トランザクションとかDaoの定義はアノテーションでやるのでナシ。

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="xxx" />
    <property name="url" value="xxx" />
    <property name="username" value="xxx" />
    <property name="password" value="xxx" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="/WEB-INF/sqlMapConfig.xml" />
</bean>

<bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

<tx:annotation-driven transaction-manager="transactionManager" />
  • Daoクラス

SqlSessionDaoSupportを継承するとSqlSessionが取得できるようになる。

@Component
public class HogeDaoImpl extends SqlSessionDaoSupport implements HogeDao {
    @Override
    public Hoge select(String sqlId, Object params) {
        return (Hoge) getSqlSession().selectOne(sqlId, params);
    }
}

で、理由はわからないけど、tx:annotation-drivenの定義とコンポーネントの自動登録
(component-scan)の定義が同じファイルないとトランザクションがきちんと反映されない
罠にはまる。トランザクションは開始してなくとも、SQLは実行できてしまうという
気付かなきゃちょっと怖いことに。


MyBatisでないサンプルだと別で書いてあったりするんだけどなぁ。
そもそも設定ファイルの読み込み順とかどう解決してるのかとか
まだまだわからんことだらけだ・・・。


参考
http://code.google.com/p/mybatis/wiki/Spring

ログ用Interceptorを作る

SpringにはDebugInterceptorやらSimpleTraceInterceptorといったものが
用意されてるけど、ログはオレオレフォーマットが好きなので、無駄に
拡張して作ってみる。


作るもの

  • MethodInterceptorを実装したクラス
  • 上記Interceptorを設定した設定ファイル
  • MethodInterceptorを実装したクラス
    • めんどいのでAbstractTraceInterceptorを継承して作る。
      • そうするとisLogEnabledをオーバーライドしないといけない・・という罠に1時間くらいはまるお約束。デフォだとtraceレベルが有効でないとログが出ない。
    • さらにめんどいので、とりあえずメッセージはあまり変えてない。
public class LogInterceptor extends AbstractTraceInterceptor {

    @Override
    protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable {
        String invocationDescription = getInvocationDescription(invocation);
        logger.info("start " + invocationDescription);
        try {
            Object rval = invocation.proceed();
            logger.info("end " + invocationDescription);
            return rval;
        } catch (Throwable ex) {
            logger.info("error " + invocationDescription, ex);
            throw ex;
        }
    }

    protected String getInvocationDescription(MethodInvocation invocation) {
        return "method '" + invocation.getMethod().getName();
    }

    @Override
    protected boolean isLogEnabled(Log logger) {
        // 強引・・・w
        return true;
    }
}
  • 設定
    • なんということはなく。
    • useDynamicLoggerとすると、そのクラスのカテゴリでログ出力される・・・ってこれって色々と大丈夫なの?
<bean id="traceInterceptor" class="jp.gr.java_conf.ykhr.sptest.LogInterceptor">
    <property name="useDynamicLogger" value="true" />
</bean>
<aop:config>
    <aop:advisor pointcut="execution(* jp.gr.java_conf.ykhr.sptest.*.*(..))"
        advice-ref="traceInterceptor" />
</aop:config>


とりあえずTODO

  • pointcutの指定の詳細は、まだよくわからん。
  • proxy-target-class="true" とするとCGLIBを使う・・・みたいだけど違いがわからん。
  • アノテーションベースで作る方法もあるようだけど、こっちの方が個人的に好きなので試してない。