動的 SQL

Mybatis の強力な機能のひとつに、動的 SQL があります。もし、JDBC や類似のフレームワークを使ったことがあるなら、条件に合うように文字列をつなぎ合わせて、スペースを忘れたり、列のリストの末尾のカンマを削除するのを忘れないように注意しながら SQL を構築するのが如何に大変か分かると思います。動的に SQL を構築するのは大変な苦痛を伴う場合があります。

動的 SQL の構築が楽しくなることはないでしょうが、MyBatis が提供する強力な動的 SQL 言語を使えばかなり改善することができます。

JSTL などの XML ベースのテキストプロセッサを使ったことがあるなら、MyBatis の動的 SQL の要素は馴染みやすいものだと思います。以前のバージョンの MyBatis では理解しておかなくてはならない要素が数多くありましたが、MyBatis 3 では改良の結果、要素の数は半分以下になっています。要素の数を減らすため、MyBatis では OGNL ベースの式(expression)を採用しています。

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

if

動的 SQL で最も良く行うのが、次のように条件に応じて where 句に検索条件を追加する処理でしょう。

<select id="findActiveBlogWithTitleLike" 
     resultType="Blog">
  SELECT * FROM BLOG 
  WHERE state = ‘ACTIVE’ 
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

このステートメントによって「任意の検索項目」を実現することができます。title を指定しなければ全ての ACTIVE な Blog が返されますが、title を指定した場合は指定したタイトルを持った Blog が返されます(このステートメントでは like 演算子を使っているので、渡された title にワイルドカードを使うこともできます)。

タイトルと著者の両方を任意の条件としたい場合はどうすれば良いのでしょうか。
ステートメント名を分かりやすいものに変更したら、あとは条件をもう一つ追加するだけです。

<select id="findActiveBlogLike" 
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’ 
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

choose, when, otherwise

全ての条件を適用する代わりに、多くの選択肢の中から一つを選んで適用したいという場合があります。

引き続き上の例を使って、タイトルが指定されたらタイトルのみを条件として検索し、著者が指定されたら著者のみを条件として検索するようにしてみましょう。どちらも指定されなかった場合は注目のブログのみを返すようにしてみましょう(ランダムに選ばれた無意味なリストではなく、管理者が戦略的に選んだリストを返したいという要件があるのでしょう)。

<select id="findActiveBlogLike" 
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

trim, where, set

ここまでの例題は動的 SQL の厄介な問題点を都合よく避けていました。もう一度 if の例に戻って、今度は "ACTIVE = 1" も動的な条件に変更してみましょう。

<select id="findActiveBlogLike" 
     resultType="Blog">
  SELECT * FROM BLOG 
  WHERE 
  <if test="state != null">
    state = #{state}
  </if> 
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

どの条件にも一致しない場合はどうなるのでしょうか?その場合は次のような SQL が実行されることになります。

SELECT * FROM BLOG 
WHERE

この SQL は構文エラーで失敗するでしょう。もし2番目の条件だけが一致したらどうなるのでしょうか?今度は次の SQL になります。

SELECT * FROM BLOG 
WHERE 
AND title like ‘someTitle’

これまた構文エラーで失敗するでしょう。動的 SQL の問題は単なる条件分岐だけで解決できるものではありません。自分で書いたことがある方なら、もう二度と書きたくないと思うはずです。

MyBatis は約 90% のケースをうまく処理できる簡単な解決策を提供します。残りの 10% についても、カスタマイズすることで処理できるようになります。上記の例は、一箇所修正するだけで期待通りに動作するようになります。

<select id="findActiveBlogLike" 
     resultType="Blog">
  SELECT * FROM BLOG 
  <where> 
    <if test="state != null">
         state = #{state}
    </if> 
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

where 要素は、内包するタグのどれかが結果を返すときだけ "WHERE" を挿入します。更に、内包するタグから返された結果が "AND" または "OR" で始まっていた場合はこれを削除します。

where 要素の動作が期待と異なる場合は、trim 要素を定義することで処理内容をカスタマイズすることができます。

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ... 
</trim>

prefixOverrides 属性にはパイプで区切られたオーバーライド対象の文字列を指定します。ここではスペースにも意味があります。trim 要素の prefixOverrides 属性のリストに含まれる文字列が先頭にあった場合は削除され、prefix 属性で指定された文字列は結果が空でない場合先頭に挿入されます。

動的な update ステートメントのために同じような要素 set が用意されています。set 要素を使うと、アップデート対象の列を動的に追加することができます。例:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

set 要素は、動的に SET キーワードを付加し、余分な末尾のカンマを削除します。

疑問に思った方のために、これと同じ処理を行う trim 要素は次のようになります。

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

prefix を追加している点は前の例と同じですが、今回は suffix をオーバーライドしている点に注意してください。

foreach

動的 SQL で良くあるもう一つの要件は、コレクションの要素をイテレーション処理したいというものです。多くの場合、IN 演算子を使った条件を構築するのが目的です。例:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

foreach 要素は非常に強力で、イテレーション処理の対象となるコレクションを指定する collection と、ループ内で要素を格納する変数 item、ループ回数を格納する index 変数を宣言することができます。また、開始・終了の文字列とイテレーションの合間に出力する区切り文字を指定することもできます。foreach タグは賢いので、余分な区切り文字を出力することはありません。

NOTE collection には Iterable を実装したオブジェクト(List や Set など)の他に Map や Array を指定することもできます。collection に Iterable または Array を指定した場合、 index で指定した変数にはインデックスの数値、 item で指定した変数にはコレクション、配列の要素が格納されます。Map あるいは Map.Entry のコレクションを指定した場合は index にマップのキー、item にマップの値が格納されます。

XML 設定ファイルと XML Mapper ファイルについての説明はここまでになります。次の章では、Java API について詳しく見ていきます。

bind

bind 要素を使うと、OGNL 式の結果を変数に格納し、その変数を SQL 文中で使用することができます。

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern} 
</select>

複数データベースのサポート

databaseIdProvider が設定されている場合、条件式で "_databaseId" 変数が利用可能となります。この変数を使うと、実行時のデータベースに応じてステートメントを使い分けることができます。oracle と db2 で異なる select 文を発行する例を次に挙げておきます。

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'db2'">
      select nextval for seq_users from sysibm.sysdummy1"
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

ダイナミック SQL 記述言語

バージョン 3.2 以降、ダイナミック SQL の記述言語が Pluggable になりました。言語ドライバーを記述することで、任意の言語でダイナミック SQL を記述することができます。

カスタムの言語ドライバーを使用する場合、まずは LanguageDriver インターフェイスを実装したクラスを作成します。

public interface LanguageDriver {
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

作成した言語ドライバーをデフォルトとして使用する場合は、mybatis-config.xml に次のような設定を追加します(typeAlias の使用は必須ではありません)。

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

lang 属性を指定することで、特定のステートメントの言語ドライバーのみを変更することもできます。

<select id="selectBlog" lang="myLanguage">
  SELECT * FROM BLOG
</select>

Mapper インターフェイスを使っている場合は @Lang アノテーションを使います。

public interface Mapper {
  @Lang(MyLanguageDriver.class)
  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}

NOTE Apache Velocity を使ってダイナミック SQL を記述することができます。MyBatis-Velocity プロジェクトを参照してください。

これまでのセクションで出てきた XML タグは、全てデフォルトの言語ドライバー org.apache.ibatis.scripting.xmltags.XmlLanguageDriver (エイリアスは xml )によって提供されているものです。