検索条件の組み立て
このレッスンでは、指定された条件だけを WHERE 句に足していく動的クエリの考え方を身に付けます。
検索フォームは条件が毎回変わります
利用者向けの検索画面を思い浮かべてください。名前で絞り込みたい人もいれば、年齢だけで絞りたい人もいて、何も入れずに一覧を見たい人もいます。つまりリクエストごとに、使われる条件の組み合わせが変わります。
Java
@GetMapping("/users")
public List<UserResponse> search(
@RequestParam(required = false) String name,
@RequestParam(required = false) Integer minAge) {
// name も minAge も来ないことがある
}この状況をメソッド名クエリで表そうとすると行き詰まります。findByName、findByMinAge、findByNameAndMinAge、findAll の4つを用意しなければならず、条件が1つ増えるたびに必要なメソッド数が倍になります。条件が5つあれば32通りです。名前で列挙する方式は、この時点で限界を迎えます。
指定されたものだけを足す
解き方は単純です。条件を最初から全部書くのではなく、指定されているものだけを順に足していきます。
Java
StringBuilder sql = new StringBuilder("SELECT * FROM users");
List<String> conditions = new ArrayList<>();
if (name != null && !name.isEmpty()) {
conditions.add("name = ?");
}
if (minAge != null) {
conditions.add("age >= ?");
}
if (!conditions.isEmpty()) {
sql.append(" WHERE ").append(String.join(" AND ", conditions));
}ここで大事なのは、条件をいったんリストに集めてから最後にまとめてつなぐ、という順序です。文字列に直接足していく書き方をすると、AND を先頭に付けるべきか否かの判定が条件ごとに必要になり、必ず事故が起きます。よくある失敗は次の2つです。
- 条件が1つも無いのに
WHEREだけが残り、SELECT * FROM users WHEREという壊れた SQL になる - 先頭の条件の前に
ANDが付いてWHERE AND name = ?になる
リストに集める方式なら、どちらも構造的に起こりません。区切り文字を挟むのは要素と要素の間だけ、という仕事を String.join に任せられるからです。
未指定をどう表すか
実務でつまずきやすいのが「未指定」の表現です。Spring の @RequestParam(required = false) を Integer で受ければ未指定は null になりますが、int で受けると未指定でも 0 が入ってしまい、区別が付きません。文字列も同様で、パラメータそのものが来ない場合と、空文字列で来た場合の両方を未指定として扱うのが普通です。
| 型 | 未指定の表し方 | 注意点 |
|---|---|---|
String | null または空文字列 | 両方を弾く必要がある |
Integer | null | int にすると 0 と区別できない |
boolean | 使わない方が良い | 未指定と false が同じになる |
Spring 本体では、この分岐をより宣言的に書くための Specification や Query by Example といった仕組みも用意されていますが、内部でやっていることは今書いた「指定された条件だけを足す」という判断そのものです。
演習で書くもの
演習では、この組み立てを自分の手で書きます。name と minAge を受け取り、指定されたものだけを含む WHERE 句を作ります。今回は引数に null を使えないので、name は空文字列を、minAge は -1 を未指定の印として扱います。どちらも未指定なら WHERE 句そのものを付けない、という判定が肝です。Spring が Specification の内側でやっている判断を、そのまま条件分岐として書き下すことになります。
要件
- name が空文字列でなければ name = 'taro' の形の条件を足すこと。値はシングルクォートで囲むこと
- minAge が -1 でなければ age >= 20 の形の条件を足すこと
- 条件が2つあるときは name の条件を先にして AND でつなぎ、条件が1つも無いときは WHERE 句を付けないこと
入出力例
buildQuery("taro", 20) → "SELECT * FROM users WHERE name = 'taro' AND age >= 20"
buildQuery("taro", -1) → "SELECT * FROM users WHERE name = 'taro'"
buildQuery("", 20) → "SELECT * FROM users WHERE age >= 20"
buildQuery("", -1) → "SELECT * FROM users"
buildQuery("hanako", 0) → "SELECT * FROM users WHERE name = 'hanako' AND age >= 0"