タイガー!タイガー!じれったいぞー!(SE編)

AS400, Java, JavaEE, JSF等の開発、習慣など。日々の気づきをまとめたブログ(備忘録)

はじめてのMybatis(with AS400)

概要

O/Rマッパーではなく、SQL文とオブジェクトのマッピングを行ってくれるというMyBatis(旧:iBatis)を試してみました。
DBはDB2 for i(AS400)となります。

結論から申しますと非常に便利!! しかしながら、DBがAS400だからなのかもしれませんが、何点か注意すべきことがあったので、学んだことをこちらにまとめておきたいと思います。

実行環境

  • Java
    • 1.8.0.45
  • Mybatis
    • 3.4.1
  • Driver : AS/400 Toolbox for Java JDBC Driver
    • Version : 8.11 (JT400 6.7)
  • Database : DB2 UDB for AS/400
    • Version : 07.01.0000 V7R1m0

はまったこと

(1) ライブラリーリストが機能しない

AS400の魅力のひとつに「ライブラリーリスト」という概念があります。
簡単にいうと、複数のスキーマを参照できるパスのようなもので、SQL文でスキーマ名を指定しなくても、そのライブラリーリストの中から順にテーブルを探してくれるという優れものなのです。
つまりは、本番環境用とテスト環境用のスキーマ(ライブラリー)を分けて運用している場合、ライブラリーリストの中身を変えるだけでSQL文は変更不要で共通化できるようになります。

ところが、今回の検証では、ライブラリーリストの設定が効きませんでした。

JDBCの設定は、下記の例の通り、DB接続オプション「libraries」で指定可能です。

Connection con = DriverManager.getConnection(
    jdbc:as400://192.168.1.5;libraries=TIGERDB,TIGERDB2, user, password);

MybatisでDB接続したジョブを調査したところ(WRKJOB - 13.ライブラリー・リストの表示)、 きちんと指定した2つのライブラリーが存在していましたが、SQLで指定したTIGERDB2に存在するテーブルを参照できず、エラーとなりました。

Cause: java.sql.SQLException: [SQL0204] TIGERDBのタイプ*FILEのTEST_TABLEが見つからない。
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:136)
    :

これはMybatisの仕様なのでしょうか。
ただし、librariesオプションの最初に指定したライブラリーのテーブルは、参照できました!

スキーマは1つだけで、限定で使っていくしかなさそうです。

なお、Mybatisでは、定義ファイルでdataSourceを複数管理できるので、こちらの指定でテストと本番の切り替えはできそうです。 安心しました!

(2) resultMapのcolumn値でピリオド付きフィールドが使えない

下記のようなSQL文をresultMapに紐づけしようとした際に、「テーブル別名.フィールド名」の部分に値が格納されませんでした。(SQLは検証時のもので故意的にJOINしています)

<mapper namespace="tigertaizo.mappers.CompanyMapper">
    <resultMap id="companyResultMap" type="tigertaizo.models.Company">
        <id property="companyCode" column="CP1.COMPANY_CODE"/>
        <result property="companyValue1" column="CP2.COMPANY_VALUE1"/>
        <result property="companyValue2" column="CP2.COMPANY_VALUE2"/>
    </resultMap>
    
    <select id="selectTest" resultMap="companyResultMap">
        SELECT
           CP1.COMPANY_CODE, 
           CP2.COMPANY_VALUE1,
           CP2.COMPANY_VALUE2
         FROM COMPANY AS CP1
         INNER JOIN COMPANY_VALUES AS CP2 ON CP1.COMPANY_CODE = CP2.COMPANY_CODE
         WHERE CP1.COMPANY_CODE = #{companyCode}
    </select>
</mapper>

これは大した問題ではなく、SQL文のフィールド名を別名にして、resultMapのcolumn値としました。解決です!

<mapper namespace="tigertaizo.mappers.CompanyMapper">
    <resultMap id="companyResultMap" type="tigertaizo.models.Company">
        <id property="companyCode" column="COMPANY_CODE"/>
        <result property="companyValue1" column="COMPANY_VALUE1"/>
        <result property="companyValue2" column="COMPANY_VALUE2"/>
    </resultMap>
    
    <select id="selectTest" resultMap="companyResultMap">
        SELECT
           CP1.COMPANY_CODE AS COMPANY_CODE, 
           CP2.COMPANY_VALUE1 AS COMPANY_VALUE1,
           CP2.COMPANY_VALUE2 AS COMPANY_VALUE2
         FROM COMPANY AS CP1
         INNER JOIN COMPANY_VALUES AS CP2 ON CP1.COMPANY_CODE = CP2.COMPANY_CODE
         WHERE CP1.COMPANY_CODE = #{companyCode}
    </select>
</mapper>

(3) 最初のN行のみの選択したい場合にN値をパラメータで渡せない

新しいDB2 for iのVersionでは「LIMIT」句が使えるようになったという話ですが、私が参照するVersionではLIMIT句は非サポート。
最初のN行取得する場合のSQL文は「FETCH FIRST N ROWS ONLY」を使うのですが、このN行をパラメータで渡せず。。。

<mapper namespace="tigertaizo.mappers.CompanyMapper">
    <resultMap id="companyResultMap" type="tigertaizo.models.Company">
        <id property="companyCode" column="COMPANY_CODE"/>
        <result property="companyValue1" column="COMPANY_VALUE1"/>
        <result property="companyValue2" column="COMPANY_VALUE2"/>
    </resultMap>
    
    <select id="selectTest" resultMap="companyResultMap">
        SELECT
           CP1.COMPANY_CODE AS COMPANY_CODE, 
           CP2.COMPANY_VALUE1 AS COMPANY_VALUE1,
           CP2.COMPANY_VALUE2 AS COMPANY_VALUE2
         FROM COMPANY AS CP1
         INNER JOIN COMPANY_VALUES AS CP2 ON CP1.COMPANY_CODE = CP2.COMPANY_CODE
         FETCH FIRST #{count} ROWS ONLY
    </select>
</mapper>

これはパラメータ渡しができないようで、SQL文字列の埋め込みをする動的SQLで対応するしかないようです。

「#{count}」の部分を 「${count}」へ変更したところ、うまく動きました!

(4) CHARフィールドのトリムがされない

JPAなどのO/Rマッパーでは、CHAR文字列のフィールドであっても、自動的にトリムされてJavaのフィールドにセットされるのですが、 Mybatis経由では、トリムされずにそのままセットされてしまいました。
JavaのDAOクラスでは、モデルにセットする際に.trim()をかましていたのですが、これは困りました。(レガシーなCHARが問題のような気もしますが・・・)

仕方なくSQL側でRTRIMすることで対処しました。大量のレコード取り出し時でのSQLパフォーマンスが気になるところです。

<mapper namespace="tigertaizo.mappers.CompanyMapper">
    <resultMap id="companyResultMap" type="tigertaizo.models.Company">
        <id property="companyCode" column="COMPANY_CODE"/>
        <result property="companyValue1" column="COMPANY_VALUE1"/>
        <result property="companyValue2" column="COMPANY_VALUE2"/>
    </resultMap>
    
    <select id="selectTest" resultMap="companyResultMap">
        SELECT
           RTRIM(CP1.COMPANY_CODE) AS COMPANY_CODE, 
           RTRIM(CP2.COMPANY_VALUE1) AS COMPANY_VALUE1,
           RTRIM(CP2.COMPANY_VALUE2) AS COMPANY_VALUE2
         FROM COMPANY AS CP1
         INNER JOIN COMPANY_VALUES AS CP2 ON CP1.COMPANY_CODE = CP2.COMPANY_CODE
         WHERE CP1.COMPANY_CODE = #{companyCode}
    </select>
</mapper>

数日試したところでは、以上4点が気になりましたが、MyBatisの魅力は十分認識できました!
JPAとうまく使い分けしていきたいところです!!!