JPA Criteria API это мощный механизм по генерации динамических и типобезопасных (при использовании Metamodel) запросов, который напрямую поддерживается в Spring Data Jpa, тем самым снимая ограничения других подходов к описанию запросов, но оставляя при это необходимый минимум автоматизации и автоматической генерации кода.
Код примеров ниже основан на коде из статьи Hello, Spring Data JPA
Спецификации и их исполнение
Spring Data JPA определяет интерфейс Specification для создания таких предикатов Criteria API, которые можно было бы использовать повторно. Интерфейс определяет ровно один метод, который должен вернуть предикат:
1
2
3
4
|
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder);
}
|
Тип T в данном случае указывает, к какой сущности относится спецификация.
Сама по себе спецификация не очень полезна, а чтобы использовать её с репозиториями, необходимо чтоб репозиторий имел в списке предков интерфейс JpaSpecificationExecutor<T> , где T — тип сущности, с которой работает репозиторий. При этом, даже если планируется работать только с запросами по примеру, наличие в предках интерфейса Repository или любого из его наследников всё равно является обязательным.
1
|
public interface PassportRepository extends CrudRepository<Passport, Long>, JpaSpecificationExecutor<Passport> { }
|
1
|
public interface PersonRepository extends CrudRepository<Person, Long>, JpaSpecificationExecutor<Person> { }
|
Интерфейс JpaSpecificationExecutor<T> определяет несколько методов:
- T FindOne(Predicate) — возвращает один объект, соответствующий условия
- Iterable<T> findAll(Predicate) — возвращает несколько объектов, соответствующих условию. Обратите внимание, что возвращается всегда Iterable<T>, без возможности уточнить тип
- long count(Predicate) — возвращает количество объектов в базе данных, соответствующих условию
- boolean exists(Predicate) — сообщает, есть ли в базе данных объект соответствующий условию
Все методы принимают объект класса Predicate, содержащий в себе условия запроса.
Пишем спецификации
Можно реализовывать класс Specification явно, но чаще используются вспомогательные классы, которые группируют различные реализации Specifation и предоставляют удобные методы для обращения к ним:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class PassportSpecification {
public static Specification<Passport> passportOwnedBy(final String lastName) {
return new Specification<Passport>() {
@Override
public Predicate toPredicate(Root<Passport> root,
CriteriaQuery<?> criteriaQuery,
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.get("owner").get("lastName"), lastName);
}
};
}
}
|
Увы, вспомогательной писанины довольно много, поэтому повторное использование кода становится важной вещью при работе со спецификациями. Впрочем, часть кода можно сократить, использовав лямбды:
1
2
3
|
public static Specification<Passport> passportOwnerStartsWith(final String lastName) {
return (r, cq, cb) -> cb.like(r.get("owner").get("lastName"), lastName+"%");
}
|
Поскольку речь идёт о прямом использовании Jpa Criteria API, сложность и гибкость спецификаций может быть сколь угодно высокой:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class PersonSpecification {
public static Specification<Person> personWorksIn(final String companyName) {
return new Specification<Person>() {
@Override
public Predicate toPredicate(Root<Person> root,
CriteriaQuery<?> criteriaQuery,
CriteriaBuilder criteriaBuilder) {
Join<Person, Company> company = root.join("workingPlaces");
return criteriaBuilder.equal(company.get("name"), companyName);
}
};
}
}
|
Использование спецификаций
Достаточно передать спецификацию в какой либо метод JpaSpecificationExecutor:
1
2
3
4
5
6
7
8
|
passportRepository.findAll(passportOwnedBy("Testoff"))
.forEach(System.out::println);
passportRepository.findAll(passportOwnerStartsWith("Te"))
.forEach(System.out::println);
personRepository.findAll(personWorksIn("Acme Ltd"))
.forEach(System.out::println);
|
При необходимости можно создавать спецификации и по месту использования, например для комбинирования уже существующих:
1
2
3
4
|
passportRepository.findAll(
where(passportOwnedBy("Testoff"))
.or(passportOwnerStartsWith("Te")))
.forEach(System.out::println);
|
Код примера доступен на github