1. はじめに
MyBatisのmapperファイル、iBatisのsqlmapファイルにはパラメータの値によって出力するSQLを変更する動的SQL機能があり、プログミングにおける条件分岐と同様と考えることができます。別の記事で書きましたが、動的SQLの評価箇所を集中的に試験・確認したい状況になりました。今回はその方法(同値検証ツール)について記載したいと思います。
同じ内容のiBatis、MyBatisのsalmapファイルを実装した場合を想定し、同じパラメータを入力して出力されるSQLを比較・検証するというものです。動的SQLを全て評価するようにパラメータを変更して複数回実行します。実行結果を見るとイメージが掴めるかと思います。
<?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>
<?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. ソースコード
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を利用している場合はそれに合わせて準備してください。
<?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ファイルを正しく動作させるために必要な設定があれば設定を追加してください。
<?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. 補足
<!-- 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ファイルの解析データと今回の記事の内容を連携した同値検証の効率化を検討する予定です。