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