Spring DATA JPA では Repository インターフェイスにメソッドを定義するだけで簡単にクエリーを実装することができるが、任意指定の検索条件があるような動的クエリーの実装には向いていない。
動的クエリーとは、検索条件の値が変化するだけでなく、検索条件そのものが変化するようなクエリーである。
例えば、ユーザーの検索条件に「ユーザー名」と「メールアドレス」の2つがある場合、findByNameAndEmail()
というメソッドをつくればいいが、検索条件が任意入力の場合、
- 両方入力 -> findByNameAndEmail()
- ユーザー名のみ入力 -> findByName()
- メールアドレスのみ入力 -> findByEmail()
- 両方未入力 -> findAll()
の4パターンの検索メソッドが必要になってしまう。
4つくらいならそれぞれに対応したfindBy***メソッドを作成してもいいが、検索条件が増えてくるととても対応しきれなくなる。
こういったケースでは、Specification を使用して「パラメータが未入力だったら無効になる検索条件」を実装すると簡単に動的クエリーを作成することができる。
Specificationは何らかの検索条件を表すインターフェイスで、実装クラスではCriteriaAPIを使用して検索条件を実装する。
Specification を使用するには、先ず Repositoryインターフェイスを JpaSpecificationExecutor を継承した形にする。
JpaSpecificationExecutor には List<T> findAll(Specification<T> spec)
等のメソッドがあり、これでSpecificationを使った検索を実行できる。
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
次に、検索条件を実装した Specification を返すメソッド群を作成する。
ここで引数がnullや空文字の時にnullを返すことでその検索条件を無効にすることができる。
import org.springframework.data.jpa.domain.Specification;
public class UserSpecifications {
/**
* 指定文字をユーザー名に含むユーザーを検索する。
*/
public Specification<User> nameContains(String name) {
return StringUtils.isEmpty(name) ? null : new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.like(root.get("name"), "%" + name + "%");
}
};
}
/**
* 指定文字をメールアドレスに含むユーザーを検索する。
*/
public Specification<User> emailContains(String name) {
:
}
}
java8のラムダ式で書くとこんな感じ。
public Specification<User> nameContains(String name) {
return StringUtils.isEmpty(name) ? null : (root, query, cb) -> {
return cb.like(root.get("name"), "%" + name + "%");
};
}
最後に呼び出し側では Specifications を使用して検索条件を結合する。
ここでは引数のnullチェック等は不要なのでそのまま呼び出せばよい。
検索条件が増えた場合には、対応するSpecificationを返すメソッドとそれを呼び出す行を追加するだけで良い。
import static UserSpecifications.*;
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))
);
}