MyBatisの動的SQLでは、検索条件の有無に応じてSQLを追加するなら if、複数の条件から1つを選ぶなら choose、リストをIN句へ展開するなら foreach を使います。
if は条件の追加、choose は条件の選択、foreach はコレクションの展開に使う、と整理すると使い分けやすくなります。
この記事では、MyBatisのMapper XMLを使い、if / choose / foreachの基本的な書き方をコード例とともに解説します。あわせて、WHERE句を整える where、更新SQLに使う set、細かな調整ができる trim、#{} と ${} を扱う際の注意点も確認します。
if・choose・foreachの使い分け
if:成立した条件をSQLへ追加するchoose / when / otherwise:複数の候補から最初に成立した1つを選ぶforeach:Listなどの要素をIN句へ展開する
たとえば、名前とステータスの両方を検索条件に加えられるようにする場合は if、メールアドレスと名前のどちらか一方を優先して使う場合は choose、複数のIDで検索する場合は foreach が適しています。
MyBatisの動的SQLを使う理由
条件によってSQLが変わる場面
検索画面では、すべての条件が常に入力されるとは限りません。ユーザー名、メールアドレス、ステータスを任意に指定できる場合は、入力された条件だけをWHERE句へ追加します。
動的SQLが使われる代表的な場面は、次のとおりです。
- 入力された検索条件だけで絞り込む
- 複数の候補から優先順位に応じて1つの条件を選ぶ
- 複数のIDをIN句へ渡す
- 変更された項目だけをUPDATEする
- 検索対象や並び順を許可された範囲で切り替える
文字列連結でSQLを生成する問題点
動的な検索条件をJavaの文字列連結で作ると、次のようなコードになりがちです。
String sql = "SELECT id, name, email, status FROM users WHERE 1 = 1";
if (condition.getName() != null) {
sql += " AND name LIKE '%" + condition.getName() + "%'";
}
if (condition.getStatus() != null) {
sql += " AND status = '" + condition.getStatus() + "'";
}この方法では、SQLの構造と条件分岐がJavaコード内に混在します。条件が増えるほど、空白やANDの付け忘れ、値のエスケープ漏れなども確認しなければなりません。
入力値を文字列連結でSQLへ直接入れる実装は、SQLインジェクションの原因にもなります。MyBatisでは、SQLの構造をMapper XMLで組み立て、検索値や更新値を通常 #{} で渡すことで、構造と値を分けて扱えます。
サンプルで使うMapper構成
以降の例では、ユーザー名、メールアドレス、ステータス、IDリストを持つ検索条件クラスを使用します。Mapper XMLのnamespaceやidの基本から確認したい場合は、次の記事も参考にしてください。

検索条件を表すJavaクラス
package com.example.dto;
import java.util.List;
public class UserSearchCondition {
private String name;
private String email;
private String status;
private List<Long> ids;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public List<Long> getIds() {
return ids;
}
public void setIds(List<Long> ids) {
this.ids = ids;
}
}XMLの test="name != null" や #{status} は、このクラスのプロパティを参照します。Java側のプロパティ名を変更した場合は、XML側の参照名も合わせて変更します。
Mapperインターフェース
package com.example.mapper;
import com.example.domain.User;
import com.example.dto.UserSearchCondition;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
List<User> findByCondition(UserSearchCondition condition);
List<User> findByIds(@Param("ids") List<Long> ids);
List<User> findByPriorityCondition(UserSearchCondition condition);
int updateUser(User user);
List<User> findBySortKey(@Param("sortKey") String sortKey);
}findByCondition と findByPriorityCondition では、検索条件オブジェクトのプロパティをXMLから直接参照します。findByIds は @Param("ids") を付けているため、XMLでは collection="ids" と指定できます。
Mapper XMLの namespace はMapperインターフェースの完全修飾名に合わせ、select や update の id は対応するメソッド名と一致させます。
ifで条件付きWHERE句を作る方法
ifの基本的な使い方
if は、test 属性の条件が成立した場合だけ、内部のSQLを追加します。複数の検索条件を同時に適用できるようにしたい場合は、それぞれを独立した if で記述します。
<select id="findByCondition"
parameterType="com.example.dto.UserSearchCondition"
resultType="com.example.domain.User">
SELECT
id,
name,
email,
status
FROM users
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
</where>
</select>name だけが入力された場合は名前の条件だけが追加されます。email と status も入力されていれば、それらの条件もANDで追加されます。
このSQL例ではCONCAT関数を使用しています。文字列結合の構文は、使用するデータベースに合わせてください。
whereと組み合わせてWHEREやANDを調整する
where 要素は、内部に有効な条件がある場合だけWHEREを追加し、先頭にあるANDやORを調整します。そのため、各条件をANDから書き始めても、生成されるSQLの不整合を防ぎやすくなります。
条件が0件になる可能性がある検索では、WHEREだけが残らないように if と where を組み合わせるのが基本です。
nullと空文字を確認する
画面から受け取る文字列は、未入力時に null になる場合と空文字になる場合があります。未入力値を検索条件へ含めないなら、両方を確認します。
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>空白だけの文字列も除外する場合は、Java側で入力を正規化する方法があります。どの値を未入力とみなすかをアプリケーション側で統一しておくと、XMLの条件を読みやすく保てます。
chooseで複数条件から1つを選ぶ方法
choose / when / otherwiseの基本形
choose は、複数の候補から成立した1つのSQLだけを選ぶ場合に使います。Javaの if / else if / else に近い構造です。
<select id="findByPriorityCondition"
parameterType="com.example.dto.UserSearchCondition"
resultType="com.example.domain.User">
SELECT
id,
name,
email,
status
FROM users
<where>
<choose>
<when test="email != null and email != ''">
email = #{email}
</when>
<when test="name != null and name != ''">
name LIKE CONCAT('%', #{name}, '%')
</when>
<when test="status != null and status != ''">
status = #{status}
</when>
<otherwise>
status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>複数の when が成立しても、選ばれるのは上から最初に成立した1つだけです。この例では、メールアドレス、名前、ステータスの順で判定します。どの条件も成立しない場合は、otherwise の条件を使用します。
ifとchooseの違い
if を複数並べると、成立した条件がすべてSQLへ追加されます。choose では、成立した候補のうち最初の1つだけが追加されます。
- 名前とステータスの両方で絞り込む:複数の
if - メールアドレスがあれば使い、なければ名前を使う:
choose
otherwise を使う場合は、条件未指定時に全件検索するのか、標準条件を付けるのかを先に決めておきます。
foreachでIN句を組み立てる方法
Listを受け取るMapper定義
複数のIDを使って検索する場合は、Mapperメソッドでリストを受け取ります。@Param("ids") を指定すると、XMLから ids という名前で参照できます。
List<User> findByIds(@Param("ids") List<Long> ids);foreachの属性
collection:繰り返すコレクションの名前item:各要素を参照する変数名open:繰り返しの前に付ける文字列separator:要素間の区切り文字close:繰り返しの後に付ける文字列
IN句へ複数のIDを渡す例
<select id="findByIds"
resultType="com.example.domain.User">
SELECT
id,
name,
email,
status
FROM users
WHERE id IN
<foreach collection="ids"
item="id"
open="("
separator=","
close=")">
#{id}
</foreach>
</select>ids に 10、20、30 を渡すと、概念的には次のSQLになります。
SELECT
id,
name,
email,
status
FROM users
WHERE id IN (?, ?, ?)各IDは #{id} によってパラメータとして扱われるため、IDを文字列連結でSQLへ埋め込む必要はありません。
空リストを渡すときの注意点
空リストではIN句に値がなくなり、不正なSQLや意図しない検索になる可能性があります。空リストを「該当なし」「条件なし」「入力エラー」のどれとして扱うかを決めてください。
空リストを条件なしとして扱う場合は、次のようにIN句の追加を分岐できます。
<where>
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids"
item="id"
open="("
separator=","
close=")">
#{id}
</foreach>
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
</where>空リストの扱いを決めないまま foreach を使うと、全件検索や不正なSQLにつながる可能性があります。Mapperを呼び出す前の入力チェックも検討してください。
setとtrimで更新SQLを組み立てる方法
setで変更項目だけを更新する
変更された項目だけをUPDATEする場合は、if と set を組み合わせます。set は、有効な項目がある場合にSET句を付け、末尾の余分なカンマを調整します。
<update id="updateUser"
parameterType="com.example.domain.User">
UPDATE users
<set>
<if test="name != null">
name = #{name},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="status != null">
status = #{status},
</if>
</set>
WHERE id = #{id}
</update>すべての更新項目がnullの場合、set が安全なUPDATEへ変換してくれるわけではありません。Mapperを呼び出す前に更新対象があるか確認してください。
また、name != null のような条件では、値をNULLへ更新できません。NULLを「未変更」と「NULLへ更新」のどちらとして扱うかは、更新用DTOなどで区別する必要があります。
trimでSQLの前後を調整する
trim は、接頭辞や末尾の文字を細かく調整できる要素です。たとえば、where に近い処理は次のように書けます。
<trim prefix="WHERE"
prefixOverrides="AND |OR ">
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
</trim>SET句の末尾にあるカンマを除去する場合は、次のように記述できます。
<trim prefix="SET"
suffixOverrides=",">
<if test="name != null">
name = #{name},
</if>
<if test="email != null">
email = #{email},
</if>
</trim>where:条件がある場合だけWHEREを付け、先頭のANDやORを調整するset:更新項目がある場合にSETを付け、末尾のカンマを調整するtrim:接頭辞や末尾文字を独自のルールで調整する
動的SQLで値を安全に扱う注意点
検索条件や更新値には#{}を使う
検索条件や更新値など、SQLへ渡す値には基本的に #{} を使います。
WHERE status = #{status}
SET email = #{email}
WHERE id = #{id}${}を安易に使わない
${} は、指定された文字列をSQLへそのまま埋め込みます。ユーザー入力を直接渡すとSQLインジェクションの原因になるため、検索値や更新値には使用しません。
<!-- ユーザー入力をそのまま渡してはいけない -->
ORDER BY ${sortKey}#{} と ${} の違いは、次の記事で詳しく解説しています。

ORDER BYを切り替える場合は許可値に限定する
カラム名やORDER BYの列名は、通常 #{} ではパラメータ化できません。動的に切り替える必要がある場合は、リクエストの文字列をそのまま渡さず、Java側で許可した値だけをSQL断片へ変換します。
public enum UserSortKey {
NAME("name"),
EMAIL("email"),
STATUS("status");
private final String sql;
UserSortKey(String sql) {
this.sql = sql;
}
public String getSql() {
return sql;
}
}public List<User> findUsers(UserSortKey sortKey) {
if (sortKey == null) {
sortKey = UserSortKey.NAME;
}
return userMapper.findBySortKey(sortKey.getSql());
}<select id="findBySortKey"
resultType="com.example.domain.User">
SELECT
id,
name,
email,
status
FROM users
ORDER BY ${sortKey}
</select>${} に渡す値は、サービス層や入力変換処理で許可リストに限定してください。ユーザーが入力した文字列をそのままSQLへ埋め込んではいけません。
MyBatisの動的SQLで起きやすいミス
パラメータ名とXMLの参照名がずれる
検索条件オブジェクトを1つ渡す場合、XMLではそのプロパティ名を参照します。
List<User> findByCondition(UserSearchCondition condition);<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>複数のパラメータやコレクションを渡す場合は、@Param で名前を明示すると対応関係を確認しやすくなります。
foreachのcollection名が合わない
@Param("ids") を指定した場合は、XMLの collection も ids にします。@Param、collection、ループ内の item をセットで確認してください。
条件未指定時の動作が決まっていない
条件が1つもない場合に、全件検索するのか、標準条件を付けるのか、入力エラーにするのかを決めておきます。更新SQLでも同様に、更新項目がない場合はMapperを呼び出さないなどの制御が必要です。
分岐を増やしすぎてSQLが読みにくくなる
動的SQLへ分岐を集めすぎると、実際に生成されるSQLを把握しにくくなります。単独条件の検索、優先順位を持つ検索、IDリスト検索など、用途が異なる処理はMapperメソッドを分ける方法もあります。
入力値の組み合わせによって生成されるSQLが変わるため、代表的な組み合わせごとにSQLとバインド値をログやテストで確認すると、条件漏れや不正なSQLを見つけやすくなります。
まとめ
MyBatisの動的SQLでは、追加したい条件の性質に応じて if、choose、foreach を使い分けます。
if:成立した検索条件をすべてSQLへ追加するchoose / when / otherwise:複数の候補から最初に成立した1つを選ぶforeach:Listなどの要素をIN句へ展開するwhere:WHEREや先頭のAND、ORを調整するset:SETや末尾のカンマを調整するtrim:接頭辞や末尾文字を独自の条件で調整する
MapperインターフェースとXMLの参照名を一致させ、null、空文字、空リスト、条件未指定時の扱いを事前に決めておくことも大切です。検索値や更新値には #{} を使い、${} が必要なSQL断片は許可した値だけに限定してください。


コメント