XML形式によるMyBatisの動的SQL構築手法

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属性の値をlistarrayに切り替えることで、柔軟なバッチ検索が可能です。

タグ: MyBatis dynamic-sql xml-mapping Java ORM

6月17日 21:34 投稿