動的SQL

MyBatisの最も強力な機能の1つは、常にその動的SQL機能でした。JDBCまたは同様のフレームワークを経験したことがある場合、SQL文字列を条件付きで連結することの煩わしさ、スペースを忘れないようにすること、または列のリストの最後にコンマを省略しないようにすることはどれほど辛いことか理解しているでしょう。動的SQLは非常に面倒な処理になる可能性があります。

動的SQLを使用しても楽しいものではありませんが、MyBatisは、マップされたSQL文内で使用できる強力な動的SQL言語で状況を確実に改善します。

動的SQL要素は、JSTLや同様のXMLベースのテキストプロセッサを使用したことがある人なら誰でも馴染みがあるはずです。以前のバージョンのMyBatisでは、多くの要素を知って理解する必要がありました。MyBatis 3はこれを大幅に改善し、今ではそれらの要素の半分以下で作業できます。MyBatisは強力なOGNLベースの式を使用して、他のほとんどの要素を排除します。

  • 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>

このステートメントは、オプションのテキスト検索タイプの機能を提供します。タイトルを入力しないと、アクティブなブログがすべて返されます。ただし、タイトルを入力すると、そのタイトルを探します(鋭い目の人にとっては、この場合、パラメータ値にマスキングまたはワイルドカード文字を含める必要があります)。

タイトルと作成者でオプションで検索したい場合はどうでしょうか?まず、より意味のあるようにステートメントの名前を変更します。次に、別の条件を追加するだけです。

<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

場合によっては、すべての条件を適用したくない場合があり、代わりに多数のオプションの中から1つのケースのみを選択したい場合があります。Javaのswitch文と同様に、MyBatisはchoose要素を提供します。

上記の例を使用しますが、今回は、タイトルが提供されている場合はタイトルのみで検索し、タイトルが提供されている場合は作成者のみで検索します。どちらも提供されていない場合は、注目すべきブログのみを返します(おそらく、無意味なランダムなブログの膨大なリストを返すのではなく、管理者によって戦略的に選択されたリスト)。

<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>

条件が1つも満たされない場合はどうなるでしょうか?次のようなSQLが生成されます。

SELECT * FROM BLOG
WHERE

これは失敗します。2番目の条件のみが満たされた場合はどうなるでしょうか?次のようなSQLが生成されます。

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

これも失敗します。この問題は条件付きでは簡単に解決できず、自分で記述したことがある場合は、二度とやりたくないでしょう。

MyBatisには、ケースの90%で機能する簡単な解決策があります。機能しない場合は、カスタマイズできます。簡単な変更を加えるだけで、すべてが正常に機能します。

<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要素を定義してカスタマイズできます。たとえば、where要素に相当するtrim要素は次のとおりです。

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

prefixOverrides属性は、パイプで区切られた上書きするテキストのリストを受け取ります。ここで、空白は重要です。その結果は、prefixOverrides属性で指定されたものの削除と、prefix属性に指定されたものの挿入です。

動的更新ステートメントには、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>

この場合、サフィックスを上書きしながら、プレフィックスを追加していることに注意してください。

foreach

動的SQLで一般的に必要なもう1つのことは、コレクションを反復処理する必要性であり、多くの場合、IN条件を作成するために使用されます。例えば

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

foreach要素は非常に強力で、コレクションを指定し、要素の本体内で使用できるアイテムとインデックス変数を宣言できます。また、開始文字列と終了文字列を指定し、反復処理の間にセパレータを追加することもできます。この要素は、余分なセパレータを誤って追加しないようにスマートです。

注記 Iterableオブジェクト(List、Setなど)、Mapオブジェクト、Arrayオブジェクトをコレクションパラメータとしてforeachに渡すことができます。IterableまたはArrayを使用する場合は、インデックスは現在の反復回数になり、値アイテムはこの反復で取得された要素になります。Map(またはMap.Entryオブジェクトのコレクション)を使用する場合は、インデックスがキーオブジェクトになり、アイテムが値オブジェクトになります。

これで、XML設定ファイルとXMLマッピングファイルに関する説明は終了です。次のセクションでは、Java APIについて詳しく説明し、作成したマッピングを最大限に活用できるようにします。

script

アノテーション付きマッパー・クラスで動的SQLを使用するには、script要素を使用できます。例えば

    @Update({"<script>",
      "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}",
      "</script>"})
    void updateAuthorValues(Author author);

bind

bind要素を使用すると、OGNL式から変数を作成し、コンテキストにバインドできます。例えば

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

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

databaseIdProviderが設定されている場合、「_databaseId」変数は動的コードで使用できるため、データベースベンダーに応じて異なるステートメントを作成できます。次の例を参照してください。

<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以降、MyBatisはプラグ可能なスクリプティング言語をサポートしているため、言語ドライバをプラグインして、その言語を使用して動的SQLクエリを作成できます。

次のインターフェースを実装することで、言語をプラグインできます。

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ファイルで設定してデフォルトとして設定できます。

<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>

または、マッパーを使用している場合は、@Langアノテーションを使用します。

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

注記 Apache Velocityを動的言語として使用できます。詳細については、MyBatis-Velocityプロジェクトを参照してください。

前のセクションで見たすべてのxmlタグは、org.apache.ibatis.scripting.xmltags.XmlLanguageDriverドライバによって提供されるデフォルトのMyBatis言語によって提供されており、xmlとしてエイリアスされています。