MyBatis 動的SQL の trim が便利なので使い方のまとめ(ついでにコード読んでみた)

アスクルの こたにん (@Kotanin0) です。

アスクルでは、O/Rマッパーに MyBatis3 を使っています。
MyBatis は、単純なCRUD操作だけでなく、 動的SQL という便利な拡張構文があります。

たとえば、本の情報を管理するテーブルから、本の一覧を抽出したいSQLがあったとします。

  • 本のタイトルを指定しなければ全件SELECT
  • 本のタイトルを指定していればWHERE句を入れてSELECT

と、本のタイトルの指定有無でif文を書きたいような場面では、次のようにif要素を利用すると解決できます。

<select>
    SELECT * FROM BOOK
    <if test="title != null">
        WHERE title LIKE #{title}
    </if>
</select>

MyBatis3動的SQL は、上のようなif要素をはじめとして、次の9種類の要素が用意されています。

  • if
  • choose
  • when
  • otherwise
  • trim
  • where
  • set
  • foreach
  • bind

その中でもtrim要素がとても便利だったので、今回はtrim要素に限定して紹介していきます。

where 要素

trim要素を説明するためには、where要素を理解しているとわかりやすいので、まずはwhere要素の説明から。
where要素は、複数のWHERE条件があったときに、いい感じに正しいSQL文を出力してくれる要素です。

where 要素のつかいかた

たとえば先ほどの本を抽出したいSQLで、本のタイトルだけでなく作者も条件に追加したいと考えました。
if要素を組み合わせて、次のような構文で解決してみます。

<select>
    SELECT * FROM BOOK
    WHERE
    <if test="title != null">
        title LIKE #{title}
    </if>
    <if test="author != null">
        AND author LIKE #{author}
    </if>
</select>

タイトルを指定しているとき、作者を指定しているときで、1つずつif要素を追加しました。
が、これにはバグが潜んでいますね。

タイトルを示すtitleが null だった場合に、次のようなSQLになってしまいます。

SELECT * FROM BOOK
WHERE
    AND author LIKE '作者'

他にも、title,authorいずれも null だった場合には、こうなってしまいます。

SELCT * FROM BOOK
WHERE

それをいい感じに解決してくれるのがwhere要素です。

<select>
    SELECT * FROM BOOK
    <where>
        <if test="title != null">
            title LIKE #{title}
        </if>
        <if test="author != null">
            AND author LIKE #{author}
        </if>
    </where>
</select>

if要素をwhere要素で囲ってあげるだけで、先ほど示した正しくないSQLも、正しいかたちに発行してくれるようになります。

これがwhere構文でした。前置き終わり。

trim 要素について

trim要素は、命名そのままに「SQL文をトリムしてくれる」という役割をもつものです。
より複雑な検索条件でSQL文を発行したいときに便利です。

trim 要素のつかいかた

たとえば、次の条件を満たす本を取得したいと考えます。

  • 本の値段が500円以上
  • タイトル・作者・概要それぞれ検索したい文字列を指定していれば条件に含める

それぞれの条件を満たすSQL文は次のようになります。

値段 タイトル 作者 概要 出力したいSQL
500 WHERE price >= 500
500 WHERE price >= 500 AND
description LIKE '概要'
500 WHERE price >= 500 AND
author LIKE '作者'
500 WHERE price >= 500 AND
(author LIKE '作者' OR description LIKE '概要')
500 WHERE price >= 500 AND
title LIKE 'タイトル'
500 WHERE price >= 500 AND
(title LIKE 'タイトル' OR description LIKE '概要')
500 WHERE price >= 500 AND
(title LIKE 'タイトル' OR author LIKE '作者')
500 WHERE price >= 500 AND
(title LIKE 'タイトル' OR author LIKE '作者' OR description LIKE '概要')

値段の条件は固定で、タイトル・作者・概要はORでつないであげる必要があります。
この場合だとwhere構文では表現が難しいです。
こんなときに便利に解決してくれるのがtrim要素です。

<select>
    SELECT * FROM BOOK
    WHERE price >= 500
    <trim prefix="AND (" prefixOverrides="OR " suffix=")">
        <if test="title != null">
            title LIKE #{title}
        </if>
        <if test="author != null">
            OR author LIKE #{author}
        </if>
        <if test="description != null">
            OR description LIKE #{description}
        </if>
    </trim>
</select>

prefixOverridesに指定した文字列が先頭に含まれていた場合は削除してくれます。
また、trim内にいずれかの文字列がある場合は、全体の先頭にprefix,全体の末尾にsuffixを挿入してくれます。

trim 要素がもつプロパティ

trim要素には4つのプロパティがあります。

プロパティ名 概要
prefix 要素内に文字列がある場合に、指定したprefixを先頭に付与する
prefixOverrides 要素内の文字列が、指定したprefixOverridesではじまる場合、その文字列を削除する
文字列は(パイプ)で複数指定が可能
suffix 要素内に文字列がある場合に、指定したsuffixを末尾に付与する
suffixOverrides 要素内の文字列が、指定したsuffixOverridesで終わる場合、その文字列を削除する
文字列は(パイプ)で複数指定が可能

これらを組み合わせることで、さまざまな複雑条件のSQLにも適用できます。

MyBatisはOSSなのでコードが公開されている

MyBatisはOSSなので、コードがGitHubで公開されています。

mybatis/mybatis-3 - GitHub

trim 要素の中身を知る

せっかくなので、コードの中身を読んでみることで、trim要素の具体的な動きを知ってみましょう。
trimの本体は TrimSqlNode.java に入っていますね。
引数で渡ってきたプロパティを利用して構文の処理をしているようです。

TrimSqlNode.java のL121-137 を見てみましょう。
ここに、trim要素の肝であるprefix,prefixOverridesの処理が入っています。

private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!prefixApplied) {
    prefixApplied = true;
    if (prefixesToOverride != null) {
      for (String toRemove : prefixesToOverride) {
        if (trimmedUppercaseSql.startsWith(toRemove)) {
          sql.delete(0, toRemove.trim().length());
          break;
        }
      }
    }
    if (prefix != null) {
      sql.insert(0, " ");
      sql.insert(0, prefix);
    }
  }
}

prefixOverridesの実体としては、.startsWith()で文字列探して.trim()しているだけですね。シンプルな文字列操作。
prefixの実体は、単純な.insert()でした。

suffix,suffixOverridesTrimSqlNode.java のL139-157 に処理が書いてありますので、ぜひご覧ください。

ついでに where 要素の中身を知る

trim要素を説明する前置きで紹介したwhere要素のコードも見てみましょう。
whereの本体は WhereSqlNode.java に入っていますね。

public class WhereSqlNode extends TrimSqlNode {

  private static List<String> prefixList = 
    Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}

なんと、where要素はtrim要素を拡張して作られていたんですね。
trim要素のprefix,prefixOverridesを明示したものがwhere要素になっていたというわけです。
trim,whereそれぞれの使い方を理解していれば納得の実装ですね。

コードを読めば使い方の幅が広がる

コードを読んでわかったことは「単純な文字列操作である」ということです。
言い換えると、WHERE,AND, ORなどのSQL文法以外でも、文字列であればなんでも指定できるということ。
今まで以上にさまざまな使い方ができるかもしれません。

まとめ

今回は MyBatis3動的SQL の紹介と、OSSの中身を読んでみました。
どんな言語やレイヤーにおいてもフレームワークは便利なので、実現したいことは機能として持っている場合が多いです。
公式リファレンスを参照することで、知らない機能を知っていけるかもしれません。

また、OSSはコード読めば、リファレンスには記載されていないより細かな挙動を知ることができます。
挙動を知ることで、隠れた機能や新たな利用方法を生み出せるかもしれません。

ASKUL Engineering BLOG

2021 © ASKUL Corporation. All rights reserved.