LoginSignup
1
0

MyBatis/iBatisの同値検証、sqlmapファイル(動的SQL)自体を単体テストする方法

Last updated at Posted at 2024-03-02

1. はじめに

MyBatisのmapperファイル、iBatisのsqlmapファイルにはパラメータの値によって出力するSQLを変更する動的SQL機能があり、プログミングにおける条件分岐と同様と考えることができます。別の記事で書きましたが、動的SQLの評価箇所を集中的に試験・確認したい状況になりました。今回はその方法(同値検証ツール)について記載したいと思います。
同じ内容のiBatis、MyBatisのsalmapファイルを実装した場合を想定し、同じパラメータを入力して出力されるSQLを比較・検証するというものです。動的SQLを全て評価するようにパラメータを変更して複数回実行します。実行結果を見るとイメージが掴めるかと思います。

image.png

ibatis-BlogMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
  PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
  "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="org.mybatis.example.BlogMapper">
  <select id="findActiveBlogLike" parameterClass="java.util.Map" resultClass="java.util.Map">
    SELECT * FROM BLOG WHERE state = 'ACTIVE'
    <isNotNull prepend="AND" property="title">
      title like #title#
    </isNotNull>
    <isNotNull prepend="AND" property="author_name">
      author_name like #author_name#
    </isNotNull>
  </select>
</sqlMap>
BlogMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="findActiveBlogLike" resultType="java.util.Map">
    SELECT * FROM BLOG WHERE state = 'ACTIVE'
    <if test="title != null">
      AND title like #{title}
    </if>
    <if test="author_name != null">
      AND author_name like #{author_name}
    </if>
  </select>
</mapper>
実行結果
------------------------------------------------
[param] {}
[expect]  SELECT * FROM BLOG WHERE STATE = 'ACTIVE'
[mybatis] SELECT * FROM BLOG WHERE STATE = 'ACTIVE'
[ibatis]  SELECT * FROM BLOG WHERE STATE = 'ACTIVE'
[mybatis:assertEquals] true
[ibatis:assertEquals]  true
[mybatis = ibatis]  true
------------------------------------------------
[param] {title=helloword}
[expect]  SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND TITLE LIKE ?
[mybatis] SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND TITLE LIKE ?
[ibatis]  SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND TITLE LIKE ?
[mybatis:assertEquals] true
[ibatis:assertEquals]  true
[mybatis = ibatis]  true
------------------------------------------------
[param] {author_name=JIRO IIDABASHI, title=helloword}
[expect]  SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND TITLE LIKE ? AND AUTHOR_NAME LIKE ?
[mybatis] SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND TITLE LIKE ? AND AUTHOR_NAME LIKE ?
[ibatis]  SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND TITLE LIKE ? AND AUTHOR_NAME LIKE ?
[mybatis:assertEquals] true
[ibatis:assertEquals]  true
[mybatis = ibatis]  true
------------------------------------------------
[param] {author_name=JIRO IIDABASHI}
[expect]  SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND AUTHOR_NAME LIKE ?
[mybatis] SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND AUTHOR_NAME LIKE ?
[ibatis]  SELECT * FROM BLOG WHERE STATE = 'ACTIVE' AND AUTHOR_NAME LIKE ?
[mybatis:assertEquals] true
[ibatis:assertEquals]  true
[mybatis = ibatis]  true

2. ポイント

  • iBatis,MyBatisが実際に行なっているSQL生成処理を呼び出して利用する
  • 動的SQLの評価を行った結果のSQLを取得するため、sqlmapファイルとしての動作を確認することができる
  • DBアクセスは行わないため動作時にデータベースを用意する必要はない
  • 必要最小限のライブラリで動作するため、軽快に動作させることができる
    • MyBatisだけであればMyBatisのライブラリのみ
    • iBatisを対象とする場合はiBatisとJDBCドライバの2つのライブラリを追加
  • 今回は説明用・軽量化のため、mainメソッドのコンソールアプリとしている
    • 注意 : mavenプロジェクトの生成後、修正漏れのためJava8で実装・動作確認

3. ソースコード

App.java
package com.example.mybatis.demo;

import java.io.Reader;
import java.io.InputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
// MyBatis
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
// iBatis
import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.client.SqlMapClientBuilder;
import com.ibatis.sqlmap.client.SqlMapSession;
import com.ibatis.sqlmap.engine.impl.SqlMapClientImpl;
import com.ibatis.sqlmap.engine.impl.SqlMapSessionImpl;
import com.ibatis.sqlmap.engine.mapping.sql.Sql;
import com.ibatis.sqlmap.engine.scope.SessionScope;
import com.ibatis.sqlmap.engine.scope.StatementScope;

public class App {
    // ⭐️ ポイント1
    private static SqlSessionFactory getSqlSessionFactory() throws IOException {
        String resource = "dummy-mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        return sqlSessionFactory;
    }

    // ⭐️ ポイント2
    private static String evaluateDynamicSql(SqlSession session, String sqlid, Object param) {
        MappedStatement mappedStatement = session.getConfiguration().getMappedStatement(sqlid);
        BoundSql boundSql = mappedStatement.getBoundSql(param);
        String sql = boundSql.getSql();
        return sql;
    }

    // ⭐️ ポイント3
    private static String trimWhiteSpaceWithUpperCase(String text) {
        if (text == null) {
            return "";
        }
        String trimCRLF = text.replaceAll("\\v", " ");
        trimCRLF = trimCRLF.replaceAll("\\h{2,}", " ");
        while (trimCRLF.indexOf(" ") == 0) {
            trimCRLF = trimCRLF.replaceFirst(" ", "");
        }
        while (trimCRLF.lastIndexOf(" ") == trimCRLF.length() -1) {
            trimCRLF = trimCRLF.substring(0, trimCRLF.length() -1);
        }
        return trimCRLF.toUpperCase();
    }

    // ⭐️ ポイント4
    private static final class SqlMapSessionImplDummy extends SqlMapSessionImpl {
        public SqlMapSessionImplDummy(SqlMapClientImpl Client) {
            super(Client);
        }
        
        public SessionScope captureSessionScope() {
            return super.sessionScope;
        }
    }

    // ⭐️ ポイント5
    private static SqlMapClient getSqlMapClient() throws java.io.IOException {
        Reader reader = com.ibatis.common.resources.Resources.getResourceAsReader("dummy-ibatis-sqlmap-config.xml");
        SqlMapClient sqlMapClient = SqlMapClientBuilder.buildSqlMapClient(reader);
        return sqlMapClient;
    }

    // ⭐️ ポイント6
    private static String evaluateDynamicSql(SqlMapClient sqlMapClient, String sqlid, Object param) {
        SqlMapClientImpl sqlMapClientImpl = (SqlMapClientImpl) sqlMapClient;
        SqlMapSessionImplDummy sqlMapSessionImplDummy = new SqlMapSessionImplDummy(sqlMapClientImpl);

        SessionScope sessionScope = sqlMapSessionImplDummy.captureSessionScope();
        StatementScope statementScope = new StatementScope(sessionScope);
        sessionScope.incrementRequestStackDepth();
        
        com.ibatis.sqlmap.engine.mapping.statement.MappedStatement mappedStatement = sqlMapSessionImplDummy.getMappedStatement(sqlid);
        mappedStatement.initRequest(statementScope);

        Sql sql = mappedStatement.getSql();
        String sqlText = sql.getSql(statementScope, param);
        return sqlText;
    }

    public static void main(String[] args) {
        try {
            // ⭐️ ポイント7
            SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
            SqlMapClient sqlMapClient = getSqlMapClient();
            SqlMapSession sqlMapSession = sqlMapClient.openSession();
            try (SqlSession session = sqlSessionFactory.openSession()) {
                // ⭐️ ポイント8
                String sqlid = "org.mybatis.example.BlogMapper.findActiveBlogLike";
                Map<String, String> param = new HashMap<>();

                System.out.println("------------------------------------------------");
                String expect01 = "SELECT * FROM BLOG WHERE state = 'ACTIVE'";
                String sql01m = evaluateDynamicSql(session, sqlid, param);
                String sql01i = evaluateDynamicSql(sqlMapClient, sqlid, param);
                System.out.println("[param] " + param);
                System.out.println("[expect]  " + trimWhiteSpaceWithUpperCase(expect01));
                System.out.println("[mybatis] " + trimWhiteSpaceWithUpperCase(sql01m));
                System.out.println("[ibatis]  " + trimWhiteSpaceWithUpperCase(sql01i));
                System.out.println("[mybatis:assertEquals] " + trimWhiteSpaceWithUpperCase(expect01).equals(trimWhiteSpaceWithUpperCase(sql01m)));
                System.out.println("[ibatis:assertEquals]  " + trimWhiteSpaceWithUpperCase(expect01).equals(trimWhiteSpaceWithUpperCase(sql01i))); 
                System.out.println("[mybatis = ibatis]  " + trimWhiteSpaceWithUpperCase(sql01m).equals(trimWhiteSpaceWithUpperCase(sql01i))); 

                System.out.println("------------------------------------------------");
                param.put("title", "helloword");
                String expect02 = "SELECT * FROM BLOG WHERE      state  = 'ACTIVE'"
                                + " AND title         like ?";
                String sql02m = evaluateDynamicSql(session, sqlid, param);
                String sql02i = evaluateDynamicSql(sqlMapClient, sqlid, param);
                System.out.println("[param] " + param);
                System.out.println("[expect]  " + trimWhiteSpaceWithUpperCase(expect02));
                System.out.println("[mybatis] " + trimWhiteSpaceWithUpperCase(sql02m));
                System.out.println("[ibatis]  " + trimWhiteSpaceWithUpperCase(sql02i));
                System.out.println("[mybatis:assertEquals] " + trimWhiteSpaceWithUpperCase(expect02).equals(trimWhiteSpaceWithUpperCase(sql02m)));
                System.out.println("[ibatis:assertEquals]  " + trimWhiteSpaceWithUpperCase(expect02).equals(trimWhiteSpaceWithUpperCase(sql02i)));
                System.out.println("[mybatis = ibatis]  " + trimWhiteSpaceWithUpperCase(sql02m).equals(trimWhiteSpaceWithUpperCase(sql02i))); 

                System.out.println("------------------------------------------------");
                param.put("author_name", "JIRO IIDABASHI");
                String expect03 = "SELECT * FROM BLOG WHERE state = 'ACTIVE'"
                                + "   AND      title     like ?"
                                + "   AND author_name    like   ?";
                String sql03m = evaluateDynamicSql(session, sqlid, param);
                String sql03i = evaluateDynamicSql(sqlMapClient, sqlid, param);
                System.out.println("[param] " + param);
                System.out.println("[expect]  " + trimWhiteSpaceWithUpperCase(expect03));
                System.out.println("[mybatis] " + trimWhiteSpaceWithUpperCase(sql03m));
                System.out.println("[ibatis]  " + trimWhiteSpaceWithUpperCase(sql03i));
                System.out.println("[mybatis:assertEquals] " + trimWhiteSpaceWithUpperCase(expect03).equals(trimWhiteSpaceWithUpperCase(sql03m)));
                System.out.println("[ibatis:assertEquals]  " + trimWhiteSpaceWithUpperCase(expect03).equals(trimWhiteSpaceWithUpperCase(sql03i)));
                System.out.println("[mybatis = ibatis]  " + trimWhiteSpaceWithUpperCase(sql03m).equals(trimWhiteSpaceWithUpperCase(sql03i))); 

                System.out.println("------------------------------------------------");
                param.remove("title");
                String expect04 = "SELECT * FROM BLOG WHERE state = 'ACTIVE'"
                                + "          AND author_name like ?";
                String sql04m = evaluateDynamicSql(session, sqlid, param);
                String sql04i = evaluateDynamicSql(sqlMapClient, sqlid, param);
                System.out.println("[param] " + param);
                System.out.println("[expect]  " + trimWhiteSpaceWithUpperCase(expect04));
                System.out.println("[mybatis] " + trimWhiteSpaceWithUpperCase(sql04m));
                System.out.println("[ibatis]  " + trimWhiteSpaceWithUpperCase(sql04i));
                System.out.println("[mybatis:assertEquals] " + trimWhiteSpaceWithUpperCase(expect04).equals(trimWhiteSpaceWithUpperCase(sql04m)));
                System.out.println("[ibatis:assertEquals]  " + trimWhiteSpaceWithUpperCase(expect04).equals(trimWhiteSpaceWithUpperCase(sql04i)));
                System.out.println("[mybatis = ibatis]  " + trimWhiteSpaceWithUpperCase(sql04m).equals(trimWhiteSpaceWithUpperCase(sql04i))); 
            }
            sqlMapSession.close();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

⭐️ ポイント1
MyBatisの一般的なお作法に則りSqlSessionFactoryのインスタンスを生成します。詳細は後述しますが、データベースにはアクセスしないのでコンフィグファイルの記載には少しだけ工夫が必要です。

⭐️ ポイント2
MyBatis版の一番の重要ポイントです。動的SQLを評価した結果のSQLはBoundSqlから取得することができます。そこまで辿り着くためにSqlSessionからsqlidに紐づくMappedStatementを取得し、MappedStatementからパラメータを指定してBoundSqlを取得するという流れになります。

⭐️ ポイント3
MyBatis、iBatisが生成するSQLはsqlmapファイルに記載されたテキストをもとにしているため、sqlmapファイルの改行や空白文字が含まれます。このままでは期待値との比較等が難しいので、単純なテキスト処理としてトリム処理を実装してしています。実際に出力されるSQLがどのようなものか興味のある方は、このメソッドを呼び出さないで確認してみてください。

⭐️ ポイント4
iBatisのためのインナークラスです。出力されるSQLを取得する処理はMyBatisの方はAPIだけで可能ですが、iBatisの方は内部実装を意識して下準備が必要になります。現在の処理で利用しているSessionScopeインスタンスが必要で、それにアクセスして取得できるようにSqlMapSessionImplを継承したクラスを実装しています。

⭐️ ポイント5
ポイント1と同様、iBatisの一般的なお作法に則りSqlMapClientのインスタンスを生成します。iBatisとMyBatisではパッケージは異なりますがクラス名は同じものが多々あります。同名のクラスはimportできないのでFQCNでクラスを指定する等の制約が発生するので注意してください。

⭐️ ポイント6
iBatis版の一番の重要ポイントです。ポイント5で生成したSqlMapClientはインターフェースで実体はSqlMapClientImplクラスのインスタンスになります。ポイント4で定義したインナークラスのインスタンスを生成するにはこのSqlMapClientImplインスタンスが必要になります。

現在の処理に紐づくSessionScopeのインスタンスを取得し、それを用いてStatementScopeインスタンスを生成します。incrementRequestStackDepthメソッドを呼び出してStatementScopeの準備は完了です。

続いてsqlidに紐づくMappedStatementを取得します。その後、initRequestメソッドで用意したStatementScopeを指定してDBアクセスのリクエスト準備を行います。

iBatisの場合はMappedStatementの後にSqlという1階層がありますが、getSqlメソッドで準備したStatementScopeインスタンスとパラメータを指定して、ようやく動的SQLを評価した結果のSQLが取得できます。

⭐️ ポイント7
準備したgetSqlSessionFactoryメソッド、getSqlMapClientメソッドを利用して必要なインスタンスを生成します。その後、それぞれsessionをオープンします。なお、iBatisは古いのでtry-with-resourcesに対応していないようです。

⭐️ ポイント8
iBatis,MyBatisで呼び出すsqlidとその際に引き渡すパラメータです。今回はsqlmapファイルの記載に合わせてMap型としています。POJOを利用している場合はそれに合わせて準備してください。

dummy-mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="org.postgresql.Driver"/>
        <property name="url" value="localhost:5432"/>
        <property name="username" value="dummy"/>
        <property name="password" value="dummy"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="BlogMapper.xml"/>
  </mappers>
</configuration>

実際にデータベースにアクセスすることありませんがdataSourceの定義は必要です。JDBCドライバは存在する値を設定しますが、URLやID/PWDはダミーの値で構いません。<mapper>に今回確認したいmapperファイルを記載します。クラス名を短縮するAlias等、mapperファイルを正しく動作させるために必要な設定があれば設定を追加してください。

dummy-ibatis-sqlmap-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <settings
        useStatementNamespaces="true"
    />
    <transactionManager type="JDBC">
        <dataSource type="SIMPLE">
            <property name="JDBC.Driver" value="org.postgresql.Driver" />
            <property name="JDBC.ConnectionURL" value="localhost:5432" />
            <property name="JDBC.Username" value="dummy" />
            <property name="JDBC.Password" value="dummy" />
        </dataSource>
    </transactionManager>
    <sqlMap resource="ibatis-BlogMapper.xml" />
</sqlMapConfig>

注意点はMyBatisの設定ファイルと同様です。

4. 補足

pom.xml
<!-- mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.13</version>
</dependency>
<!-- ibatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis2</artifactId>
    <version>2.3.7</version>
</dependency>
<!-- postgresql -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.6.0</version>
</dependency>
関連コマンド
# プロジェクトを生成するmavenコマンド
mvn archetype:generate ^
-DinteractiveMode=false ^
-DarchetypeArtifactId=maven-archetype-quickstart ^
-DgroupId=com.example.mybatis.demo ^
-DartifactId=mybatis-demo

# お試しビルドするmavenコマンド
mvn package -Dmaven.test.skip=true

# 依存ライブラリをダウンロードするmavenコマンド
mvn dependency:copy-dependencies -DoutputDirectory=lib

# 今回のアプリを実行するコマンド (MyBatis,iBatis両方の記載がある場合)
java -cp .\lib\mybatis-3.5.13.jar;.\lib\mybatis2-2.3.7.jar;.\lib\postgresql-42.6.0.jar;.\target\mybatis-demo-1.0-SNAPSHOT.jar com.example.mybatis.demo.App

# 今回のアプリを実行するコマンド (iBatisの記載はなくMyBatisのみの場合)
java -cp .\lib\mybatis-3.5.13.jar;.\target\mybatis-demo-1.0-SNAPSHOT.jar com.example.mybatis.demo.App

5. さいごに

実際のシステム開発では多くの動的SQLが利用されていたり、タグの入れ子が深いsqlmapファイルもかなり存在します。またsqlmapファイル自体がそもそも数千〜数万もあったりします。WebやBatch等のアプリケーションを起動させてsalmapファイルの動的SQLを評価するにはリソース負荷が大きい(動作に時間が掛かる、処理が重い等)ため、sqlmapファイルを単体で動作・検証する仕組みを今回検討しました。またiBatis,MyBatisという新旧ライブラリを同時に実行させるのは結構大変ということも久々に実感しました。今後は別の記事で記載したsqlmapファイルの解析データと今回の記事の内容を連携した同値検証の効率化を検討する予定です。

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0