MyBatisの動的SQL入門|if / choose / foreachの使い方を解説

MyBatisで開発していると、検索条件が少ないうちは困らなくても、条件が増えたあたりから急にSQLが扱いづらくなります。

たとえば、名前が入っていたら条件を付ける、ステータスが指定されていたら絞り込む、IDの一覧があればIN句で検索する、といった処理です。こうした可変条件をJava側の文字列連結で組み立て始めると、スペースの入れ忘れやANDの付け方で崩れやすくなります。MyBatisの公式ドキュメントでも、条件付きの文字列連結は扱いづらくなりやすい点が説明されています。

MyBatisの動的SQLは、そうした可変条件をMapper XMLの中で整理しやすくする仕組みです。主な要素として、if / choose / trim(where / set)/ foreach が用意されています。

この記事では、文法をただ並べるのではなく、実務でどのように使い分けるかが伝わるように整理して説明します。

目次

MyBatisの動的SQLが必要な理由

ここでは、動的SQLがなぜ必要になるのかを説明します。

条件検索はそのままだと複雑になりやすい

固定条件だけのSQLなら、Mapper XMLにそのまま書いても特に困りません。
ただ、実務では「条件が入っているときだけ絞り込む」検索がよく出てきます。

たとえば、次のような検索です。

  • 名前が指定されていれば名前で絞る
  • ステータスが指定されていればステータスで絞る
  • 登録日が指定されていれば期間条件を付ける
  • ID一覧が指定されていればIN句で絞る

このあたりを全部固定SQLで表現しようとすると、検索メソッドを細かく増やすことになりがちです。
一方で、1つのSQLにまとめようとして雑に条件分岐を入れると、今度はSQLの見通しが悪くなります。

文字列連結でSQLを組み立てる問題点

Java側でSQL文字列を組み立てる方法もありますが、保守しやすいとは言いづらいです。

特に困りやすいのは、次のような点です。

  • WHEREの有無を自分で制御しないといけない
  • 先頭のAND / ORが不自然になりやすい
  • 更新SQLでは末尾カンマが残りやすい
  • 条件追加のたびに読みづらくなる

MyBatisの公式ドキュメントでも、条件付きSQLの組み立てでは、スペースや末尾カンマの扱いがつらい点が挙げられています。

動的SQLで整理すると何が楽になるか

MyBatisの動的SQLを使うと、条件ごとの出し分けをXML側で整理できます。

たとえば公式では、if で条件付きのWHERE句を追加し、where で先頭のAND / ORを自動的に整える方法が紹介されています。set は更新文の末尾カンマを整理し、foreach はコレクションを回してIN句を組み立てる用途で使われます。

つまり、動的SQLは「なんでも動的にする仕組み」ではなく、可変になりやすい部分だけを安全に扱うための仕組みとして使うのが基本です。

よく使う動的SQLの基本

ここでは、実務でよく使う動的SQLの基本を説明します。

if / whereで検索条件を組み立てる

まず一番よく使うのが if です。
条件があるときだけSQLの一部を出したい場面で使います。

<select id="findUsers" resultType="User">
  SELECT
    id,
    name,
    status,
    created_at
  FROM users
  <where>
    <!-- name があるときだけ条件を追加 -->
    <if test="name != null and name != ''">
      AND name LIKE CONCAT('%', #{name}, '%')
    </if>    <!-- status があるときだけ条件を追加 -->
    <if test="status != null">
      AND status = #{status}
    </if>
  </where>
</select>

この例のポイントは、if だけではなく where と組み合わせていることです。

where は、中に条件が1つでも出力されたときだけ WHERE を付けてくれます。さらに、条件の先頭が ANDOR で始まっていても、それを取り除いてくれます。これはMyBatis公式でも明示されています。

逆に言うと、可変条件の検索で if だけを並べて WHERE を手書きすると、条件が1つもないときに壊れたSQLになりやすいです。

chooseで条件分岐を整理する

if は便利ですが、全部の条件を並列で評価したいときに向いています。
一方で、複数の候補からどれか1つだけ適用したいときは choose のほうが意図がはっきりします。

<select id="findUsersByPriorityCondition" resultType="User">
  SELECT
    id,
    name,
    email,
    status
  FROM users
  <where>
    <choose>
      <!-- name があれば name を優先 -->
      <when test="name != null and name != ''">
        name LIKE CONCAT('%', #{name}, '%')
      </when>      <!-- name がなければ email を使う -->
      <when test="email != null and email != ''">
        email = #{email}
      </when>      <!-- どちらもなければ有効ユーザーだけ返す -->
      <otherwise>
        status = 'ACTIVE'
      </otherwise>
    </choose>
  </where>
</select>

choose / when / otherwise は、Javaの switch のように、複数候補のうち1つを選ぶための要素として公式で説明されています。

この書き方が向いているのは、たとえば次のようなケースです。

  • フリーワード検索があればそれを優先する
  • それがなければメールアドレスで検索する
  • それもなければ最低限の条件だけにする

全部を if で並べても動くことはありますが、優先順位がある条件choose のほうが読みやすくなります。

setで更新SQLを組み立てる

更新処理では、入力された項目だけを更新したいことがあります。
このときに便利なのが set です。

<update id="updateUser">
  UPDATE users
  <set>
    <!-- name があるときだけ更新対象にする -->
    <if test="name != null and name != ''">
      name = #{name},
    </if>    <!-- email があるときだけ更新対象にする -->
    <if test="email != null and email != ''">
      email = #{email},
    </if>    <!-- status があるときだけ更新対象にする -->
    <if test="status != null">
      status = #{status},
    </if>
  </set>
  WHERE id = #{id}
</update>

この例では、各行の末尾にカンマを書いています。
普通に考えると最後のカンマが残りそうですが、set は更新文向けに末尾カンマを整えるための要素として用意されています。where と同じく、trim 系の便利要素の1つです。

更新SQLを手書きで動的に組み立てると、最後のカンマ処理が地味に面倒です。
その処理を任せられるのが set の強みです。

foreachでIN句を扱う

一覧のIDで絞り込みたい場面では、foreach がよく使われます。
公式でも、コレクションを反復してIN条件を組み立てる用途が紹介されています。

<select id="findUsersByIds" resultType="User">
  SELECT
    id,
    name,
    status
  FROM users
  <where>
    <foreach
      collection="ids"
      item="id"
      open="id IN ("
      separator=","
      close=")">
      #{id}
    </foreach>
  </where>
</select>

たとえば ids = [1, 3, 5] なら、イメージとしては次のようなSQLになります。

id IN (1, 3, 5)

foreach は、IN句だけではなく、複数値を順番に並べたい場面でも応用できます。
ただし、入門段階ではまずコレクションからIN句を作る用途を押さえておけば十分です。

実務でよくある書き方と注意点

ここでは、実務でよくある使い方と、ハマりやすい点を説明します。

複数条件検索をどう整理するか

複数条件検索では、全部をそのまま if で並べればよいとは限りません。
大事なのは、条件の性質ごとに整理することです。

たとえば、次のように分けて考えると見通しがよくなります。

  • 並列で追加される条件
    例: 名前、ステータス、登録日
  • 優先順位がある条件
    例: フリーワードがあればそちらを優先
  • リストで渡される条件
    例: ID一覧、ステータス一覧

全部を1つの巨大なSQLに押し込むと、あとで読む人がかなりつらくなります。
条件数が増えてきたら、検索条件オブジェクトを見直したり、用途別にSQLを分けることも考えたほうがよいです。

空値 / 空リストをどう扱うか

ここは実務でかなり重要です。

たとえば name が空文字なのに LIKE '%%' のような条件が付くと、意図しない全件検索に近い動きになることがあります。
そのため、文字列系は null だけでなく空文字も考慮しておくほうが安全です。

<if test="name != null and name != ''">
  AND name LIKE CONCAT('%', #{name}, '%')
</if>

foreach でも同様で、空リストの扱いを雑にすると困ります。
MyBatis公式の例では、foreachnullable="true" を付けたサンプルが示されています。これはコレクションが null の場合の扱いを考慮した書き方です。

ただし、実務では null を許すか、空リストなら検索しないのか、0件として扱うのかをアプリ側で明確に決めることが大切です。
ここが曖昧だと、「条件なし検索になった」「逆に何も返らなかった」といったズレが起きやすくなります。

WHERE / AND / カンマまわりでハマりやすい点

動的SQLでよくあるミスは、だいたい次の3つです。

  • 条件が1つもなくて WHERE だけ残る
  • 先頭に AND が付いて壊れる
  • 更新SQLで末尾カンマが残る

この3つは、まさに whereset が解決してくれる領域です。
公式でも、where は条件があるときだけ WHERE を付け、先頭の AND / OR を除去する仕組みとして説明されています。

つまり、動的SQLでハマりにくくするコツは、気合いで整えることではなく、用意されている要素を素直に使うことです。

動的SQLを読みやすく保つコツ

ここでは、動的SQLを保守しやすくする考え方を説明します。

条件を増やしすぎない

動的SQLは便利ですが、便利だからといって何でも1つのSQLに集めると読みにくくなります。

たとえば、検索画面の条件を全部1本のMapperメソッドに詰め込むと、XMLが長くなりすぎて、どの条件が本当に必要なのか見えにくくなります。

そんなときは、次のように考えると整理しやすいです。

  • 一覧検索用
  • 詳細検索用
  • 管理画面用
  • CSV出力用

利用場面が違うなら、SQLを分けたほうが読みやすいことは多いです。
動的SQLは「まとめるため」ではなく、必要な範囲で可変条件を扱うために使うのがちょうどよいです。

検索条件オブジェクトとMapperの責務を分ける

検索条件が増えてきたら、引数をバラバラに渡すより、検索条件用のDTOにまとめたほうが扱いやすくなります。

public class UserSearchCondition {
    private String name;
    private String email;
    private String status;
    private List<Long> ids;    // getter / setter は省略
}
<select id="findUsersByCondition" parameterType="UserSearchCondition" resultType="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>
    <if test="ids != null and ids.size() > 0">
      AND id IN
      <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
      </foreach>
    </if>
  </where>
</select>

この形にすると、どんな条件を受け取る検索なのかが見えやすくなります。
Java側で条件を整理し、MapperではSQLとしての表現に集中させると、役割分担がきれいになります。

動的にしすぎない判断も大事

ここは意外と大切です。

たとえば、条件が最初から固定で、今後も増える予定がないSQLまで動的SQLにすると、かえって読みづらくなることがあります。
また、複雑な分岐を無理に1本へまとめるより、用途ごとにSQLを分けたほうが安全な場合もあります。

MyBatisの動的SQLは強力ですが、万能ではありません。
だからこそ、固定でよい部分は固定のままにする判断が大事です。

使うべき場面は、次のようなケースです。

  • 条件検索で可変項目が複数ある
  • 更新対象が入力内容によって変わる
  • コレクションを使ったIN検索が必要
  • 優先条件の分岐をSQL側で表現したい

逆に、毎回同じ形で実行される単純なSQLなら、無理に動的にしないほうがすっきりします。

まとめ

MyBatisの動的SQLは、条件検索や更新SQLを柔軟に書くための機能です。
特に、if は条件付き追加、choose は分岐、where はWHERE句の整形、set は更新文の整形、foreach はIN句の組み立てで役立ちます。これらが主要な要素として公式に整理されています。

ただし、便利だからといって全部を1本のSQLに詰め込むと、今度は保守がつらくなります。
可変条件を安全に扱うために使うという目的を忘れず、必要な範囲で整理していくことが大切です。

最初の一歩としては、まず次の流れを押さえれば十分です。

  1. 検索条件には ifwhere を使う
  2. 優先条件には choose を使う
  3. 更新処理には set を使う
  4. IN句には foreach を使う

この4つをきちんと使い分けられるようになるだけでも、MyBatisのSQLはかなり読みやすくなります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次