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.