アスクルの こたにん (@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で公開されています。
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,suffixOverrides も TrimSqlNode.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はコード読めば、リファレンスには記載されていないより細かな挙動を知ることができます。
挙動を知ることで、隠れた機能や新たな利用方法を生み出せるかもしれません。