読者です 読者をやめる 読者になる 読者になる

8t's BBR

よくつまったあれこれをメモ

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(なんとか)とかを何個もつくらなくてよくなるそうです。


なるほど、すごいですね(適当)。



※今回の記事は、いつにも増して特にうる覚えで書きました。
 専門の人なら「なんやこいつ。トンチンカンやな」と仰られるかもしれません。
 コードも間違っている可能性が大いにあります。
 どなたかの参考になれば幸いですが、ざっくり雰囲気だけにしていただくことをお勧めします。