MyBatisにおけるXMLベースの動的SQL構築
MyBatisのXMLマッピングファイルでは、条件分岐や反復処理を用いてSQL文を動的に生成する機能が提供されています。これにより、実行時のパラメータに応じて柔軟にクエリを調整でき、コードの重複を大幅に削減できます。以下に、主要な動的SQLタグの使い方と実装時のポイントを示します。
条件分岐によるクエリ制御(<if>)
検索機能などで、パラメータがnullまたは空でない場合のみWHERE句の条件を追加したいケースは頻繁に発生します。XML内では<if>タグを使用し、OGNL式で評価を行います。
<!-- 単一文字列パラメータの場合 -->
<select id="searchItemsByKeyword" parameterType="string" resultType="Item">
SELECT * FROM items WHERE 1=1
<if test="_parameter != null and _parameter != ''">
AND item_name LIKE #{_parameter}
</if>
</select>
<!-- マップパラメータとbindタグの組み合わせ -->
<select id="searchItemsByKeywordMap" parameterType="map" resultType="Item">
<bind name="searchPattern" value="'%' + _parameter.keyword + '%'" />
SELECT * FROM items WHERE 1=1
<if test="_parameter.keyword != null and _parameter.keyword != ''">
AND item_name LIKE #{searchPattern}
</if>
</select>
単一引数(StringやIntegerなど)を渡す場合、OGNL式での参照は_parameterを使用する必要があります。プロパティ名を直接指定すると、There is no getter for property named ... というエラーが発生します。また、<bind>タグを利用すれば、SQL内でワイルドカードを安全に付与できるため、Java側で文字列連結を行う手間が省けます。
String configPath = "mybatis-config.xml";
try (Reader reader = Resources.getResourceAsReader(configPath)) {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
try (SqlSession session = factory.openSession()) {
String queryId = "com.example.mapper.ItemMapper.searchItemsByKeywordMap";
Map<String, Object> params = new HashMap<>();
params.put("keyword", "tech");
List<Item> results = session.selectList(queryId, params);
results.forEach(System.out::println);
}
} catch (IOException e) {
e.printStackTrace();
}
排他的条件分岐(<choose> / <when> / <otherwise>)
複数の検索オプションから一つだけ選択し、該当する列に対してフィルタを適用する場合、<choose>ブロックが有効です。これはJavaのswitch-case文に近い挙動をします。
<select id="queryLogsByFilter" parameterType="map" resultType="LogRecord">
<bind name="matchPattern" value="'%' + _parameter.value + '%'" />
SELECT * FROM system_logs WHERE 1=1
<choose>
<when test="_parameter.mode == 'level'">
AND log_level LIKE #{matchPattern}
</when>
<when test="_parameter.mode == 'message'">
AND log_message LIKE #{matchPattern}
</when>
<otherwise>
AND log_level LIKE #{matchPattern} OR log_message LIKE #{matchPattern}
</otherwise>
</choose>
</select>
Java側では、検索モードを示すキーとキーワードをマップに格納して渡します。一致しない場合や未指定の場合は<otherwise>内の条件が適用されます。
動的な句の生成と不要な区切り文字の除去(<trim>, <where>, <set>)
レコードの更新時など、条件に応じて適用する列を変えたい場合、すべての項目を固定で記述するのは非効率的です。<trim>タグは、生成されたSQL断片に対して接頭辞・接尾辞の追加や、不要な文字の自動削除を行います。
<update id="modifyProjectDetails" parameterType="map">
UPDATE projects
<trim prefix="SET" suffixOverrides=",">
<if test="_parameter.projectName != null and _parameter.projectName != ''">
project_name = #{projectName},
</if>
<if test="_parameter.budget != null">
budget = #{budget},
</if>
</trim>
<trim prefix="WHERE" prefixOverrides="AND | OR">
<if test="_parameter.projectId != null">
AND project_id = #{projectId}
</if>
<if test="_parameter.status != null">
AND status = #{status}
</if>
</trim>
</update>
suffixOverrides=","によりSET句の末尾カンマが、prefixOverrides="AND | OR"によりWHERE句の先頭論理演算子が自動的に削除されるため、構文エラーを防げます。注意点として、prefixOverridesなどの属性名は大文字小文字を正しく記述する必要があります。また、実用上は<where>や<set>タグを直接使用することも可能です。
Map<String, Object> updateParams = new HashMap<>();
updateParams.put("projectName", "NewSystem");
updateParams.put("budget", 500000);
updateParams.put("projectId", 101);
try (SqlSession session = factory.openSession()) {
int affectedRows = session.update("com.example.mapper.ProjectMapper.modifyProjectDetails", updateParams);
System.out.println("更新件数: " + affectedRows);
}
コレクションの展開とIN句の構築(<foreach>)
複数のIDを指定して一括取得する場合など、リストや配列をSQLのIN句に変換するには<foreach>タグを使用します。主な属性は以下の通りです。
collection: 反復対象のコレクション名item: 現在の要素を格納する変数名index: 現在のインデックス変数名open/close: 生成されるリストの前後に付与する文字separator: 要素間の区切り文字
<select id="retrieveTransactions" parameterType="map" resultType="Transaction">
SELECT * FROM transactions WHERE txn_id IN
<foreach collection="targetIds" item="tid" open="(" separator="," close=")">
#{tid}
</foreach>
</select>
パラメータの型によってcollection属性のデフォルト値が変化します。
マップ経由でリストを渡す場合:
List<String> ids = Arrays.asList("TXN-001", "TXN-002", "TXN-003");
Map<String, Object> queryMap = new HashMap<>();
queryMap.put("targetIds", ids);
try (SqlSession session = factory.openSession()) {
List<Transaction> result = session.selectList("com.example.mapper.TransactionMapper.retrieveTransactions", queryMap);
result.forEach(System.out::println);
}
単一引数としてリストを渡す場合:
<!-- XMLマッピング -->
<select id="retrieveTransactionsByList" parameterType="list" resultType="Transaction">
SELECT * FROM transactions WHERE txn_id IN
<foreach collection="list" item="tid" open="(" separator="," close=")">
#{tid}
</foreach>
</select>
<!-- Java側 -->
List<String> ids = Arrays.asList("TXN-001", "TXN-002");
try (SqlSession session = factory.openSession()) {
List<Transaction> result = session.selectList("com.example.mapper.TransactionMapper.retrieveTransactionsByList", ids);
}
配列を渡す場合:
<!-- XMLマッピング -->
<select id="retrieveTransactionsByArray" parameterType="arraylist" resultType="Transaction">
SELECT * FROM transactions WHERE txn_id IN
<foreach collection="array" item="tid" open="(" separator="," close=")">
#{tid}
</foreach>
</select>
<!-- Java側 -->
String[] codes = {"TXN-004", "TXN-005"};
try (SqlSession session = factory.openSession()) {
List<Transaction> result = session.selectList("com.example.mapper.TransactionMapper.retrieveTransactionsByArray", codes);
}
このように、引数の渡し方に応じてcollection属性の値をlistやarrayに切り替えることで、柔軟なバッチ検索が可能です。