Springフレームワークでのクエリの書き方
Spring JTA DATAを使ってクエリを書くことに最近挑戦していました。
まぁ初心者には難しい。
挫折寸前ですね。
Spring Data JPA の Specificationでらくらく動的クエリー - Qiita
この記事とかは非常に参考になるのですが、それでも頭パンクしていました。
で、今回の記事は技術的な話というよりかは、アイデア的な話で、
せっかく教えていただいたことを忘れないようにと、今回も備忘録として残します。
さて、先ほど紹介した記事では、
動的クエリを作成するにあたってSpecificationを利用しています。
Specificationは仕様という意味ですが、これの使い方とかよくわからなかった。
なので例えば、年齢や性別などの検索条件を満たすUserデータを全て取ってくる
findAllメソッドを書くとしたら、このような感じに愚直に書いていました。
public class UserService { public List<User> findUsers(String name, Integer id, Integer age, Boolean isMale) { return userRepository.findAll(Specifications .where(new Specification<User>() { @Override public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.le(root.get("id"), 1000); }}) .and(new Specification<User>() { @Override public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.ge(root.get("age"), 20); }}) .and(new Specification<User>() { @Override public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.isTrue(root.get("isMale")); }}) .or(new Specification<User>() {} : : }
それぞれのパラメータについて、1つ1つandやorを繋げていってます。これだと、
(id <= 1000) && (age >= 20) && isMale || (... )
というような感じの条件ですかね。
こんな感じでもできなくはないみたいです。
ただ、これだと困ることがあります。
- 条件が複雑になった場合、andやorを使いすぎてごちゃごちゃに(そして混乱する)
- やたらと長いコードになりやすい
- 条件が少し変わるだけでも、下手に変えると大変なことに
- 将来、この書き方だとfindByAgeとか色々メソッド作らなければ・・・
一番下の話については後程少し触れます。
一番上の話については、もうすでにそうなっていますね。
今ならまだ理解できますが条件A、B、C、・・・について
(A and B) and (C or { D ? E : F } )
ならどう書けばいいでしょうか。
やってみて即答できる人は決して多くないと信じています。
テストのしづらさもあって、私は実際にここで丸1日つぶしました。
答えの1つとしては、
public List<User> findUsers(String name, Integer age, Boolean isMale) { return userRepository.findAll(Specifications .where(new Specification<User>() { @Override public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.and(cb.le(root.get("id"), 1000), cb.ge(root.get("age"), 20)); // A and B }}) .and(new Specification<User>() { @Override public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) { isMale // D ? return cb.or( /*C*/ , /*E*/ ); : return cb.or( /*C*/ , /*F*/ ); }}) ); }
的な感じのものがあります。(確認してないので、間違っていたらすみません。)
この解答でのポイントは、
- Specificationをorで結ばない
- CriteriaBuilderに頑張らせる
かと思っています。
特に上の意識は大事で、orは極力使わない方がいいと思います。
理由はやればわかります、下手すれば地獄を見ます。
正直なところ、これではベテランの方々に笑われるのでしょう。
せっかくのSpecificationの意味がなくね?(笑)と。
Specificationに名前をつけれるんだから、それで満たすべきSpecificationを定義して
それをandで繋げばいいじゃん、と。
ここでようやく、先ほどのページの話が更に輝きを放ってくれます。
Spring Data JPA の Specificationでらくらく動的クエリー - Qiita(再掲)
下のコードは丸パクリで恥ずかしいですが、コメントだけつけさせていただくと
// まずはSpecificationを定義 public Specification<User> nameContains(String name) { return StringUtils.isEmpty(name) ? null : (root, query, cb) -> { return cb.like(root.get("name"), "%" + name + "%"); }; } public Specification<User> emailContains(String email) { : } : // これで見た目もスッキリ。どういう仕様を満たせばいいかも一目でわかる。 public List<User> findUsers(String name, String email, Tag followTag, Long ContributionCount) { return userRepository.findAll(Specifications .where(nameContains(name)) .and(emailContains(email)) .and(flolowTagsHas(followTag)) .and(contributionCountGreaterThan(Contribution)) ); }
という感じになるのです。ここから先はもう本当によくわかってないので、
間違ったこと言ってるかもしれないですが、Specificationを定義するときに、
コードの例のようにしたり、Optional型を使うことで、条件に使う引数がnullでも
判定ができるようになったりするとか・・・。
それによって、findBy(なんとか)とかを何個もつくらなくてよくなるそうです。
なるほど、すごいですね(適当)。
※今回の記事は、いつにも増して特にうる覚えで書きました。
専門の人なら「なんやこいつ。トンチンカンやな」と仰られるかもしれません。
コードも間違っている可能性が大いにあります。
どなたかの参考になれば幸いですが、ざっくり雰囲気だけにしていただくことをお勧めします。