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