1. Введение
Spring Data JPA предоставляет множество способов работы с сущностями, включая методы запросов и пользовательские запросы JPQL . Но иногда нам нужен более программный подход, такой как Criteria API или QueryDSL .
Criteria API предлагает программный способ создания типизированных запросов , что помогает нам избежать синтаксических ошибок. Кроме того, когда мы используем его с Metamodel API, он выполняет проверки во время компиляции, чтобы подтвердить, что мы использовали правильные имена и типы полей.
Однако у него есть свои недостатки; нам приходится писать многословную логику, раздутую шаблонным кодом.
В этом руководстве мы узнаем, как реализовать нашу пользовательскую логику DAO с помощью запросов критериев. Мы также покажем, как Spring помогает сократить шаблонный код.
2. Образец заявления
Для простоты в примерах мы будем реализовывать один и тот же запрос несколькими способами: поиск книг по имени автора и названию, содержащему String
.
Вот объект Book :
@Entity
class Book {
@Id
Long id;
String title;
String author;
// getters and setters
}
Поскольку мы хотим, чтобы все было просто, в этом руководстве мы не будем использовать Metamodel API.
3. Класс @Repository
Как мы знаем, в компонентной модели Spring мы должны размещать нашу логику доступа к данным в компонентах @Repository
. Конечно, эта логика может использовать любую реализацию, например Criteria API.
Для этого нам нужен только экземпляр EntityManager
, который мы можем подключить автоматически:
@Repository
class BookDao {
EntityManager em;
// constructor
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Book> cq = cb.createQuery(Book.class);
Root<Book> book = cq.from(Book.class);
Predicate authorNamePredicate = cb.equal(book.get("author"), authorName);
Predicate titlePredicate = cb.like(book.get("title"), "%" + title + "%");
cq.where(authorNamePredicate, titlePredicate);
TypedQuery<Book> query = em.createQuery(cq);
return query.getResultList();
}
}
Приведенный выше код следует стандартному рабочему процессу Criteria API:
- Во-первых, мы получаем ссылку
CriteriaBuilder
, которую мы можем использовать для создания различных частей запроса. - Используя
CriteriaBuilder
, мы создаемCriteriaQuery<Book>
, который описывает, что мы хотим сделать в запросе. Он также объявляет тип строки в результате. - С помощью
CriteriaQuery<Book>
мы объявляем начальную точку запроса ( сущностьBook
) и сохраняем ее в переменнойbook для последующего использования.
- Затем с помощью
CriteriaBuilder
мы создаем предикаты для нашей сущностиBook .
Обратите внимание, что эти предикаты еще не имеют никакого эффекта. - Мы применяем оба предиката к нашему
CriteriaQuery.
CriteriaQuery.where(Predicate…)
объединяет свои аргументы в логическийи
. Это тот момент, когда мы привязываем эти предикаты к запросу. - После этого мы создаем
экземпляр TypedQuery<Book>
из нашегоCriteriaQuery.
- Наконец, мы возвращаем все соответствующие объекты
Book .
Обратите внимание, что, поскольку мы пометили класс DAO с помощью @Repository
, Spring позволяет перевод исключений для этого класса.
4. Расширение репозитория с помощью пользовательских методов
Наличие автоматических пользовательских запросов — мощная функция Spring Data. Однако иногда нам нужна более сложная логика, которую мы не можем создать с помощью автоматических методов запросов.
Мы можем реализовать эти запросы в отдельных классах DAO (как в предыдущем разделе).
Или, если мы хотим, чтобы интерфейс @Repository
имел метод с пользовательской реализацией, мы можем использовать компонуемые репозитории .
Пользовательский интерфейс выглядит так:
interface BookRepositoryCustom {
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title);
}
А вот интерфейс @Repository
:
interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {}
Мы также должны изменить наш предыдущий класс DAO, чтобы реализовать BookRepositoryCustom,
и переименовать его в BookRepositoryImpl
:
@Repository
class BookRepositoryImpl implements BookRepositoryCustom {
EntityManager em;
// constructor
@Override
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
// implementation
}
}
Когда мы объявляем BookRepository
как зависимость, Spring находит BookRepositoryImpl
и использует его при вызове пользовательских методов.
Допустим, мы хотим выбрать, какие предикаты использовать в нашем запросе. Например, когда мы не хотим находить книги по автору и названию, нам нужно только совпадение автора.
Есть несколько способов сделать это, например, применить предикат только в том случае, если переданный аргумент не равен null
:
@Override
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Book> cq = cb.createQuery(Book.class);
Root<Book> book = cq.from(Book.class);
List<Predicate> predicates = new ArrayList<>();
if (authorName != null) {
predicates.add(cb.equal(book.get("author"), authorName));
}
if (title != null) {
predicates.add(cb.like(book.get("title"), "%" + title + "%"));
}
cq.where(predicates.toArray(new Predicate[0]));
return em.createQuery(cq).getResultList();
}
Однако такой подход усложняет сопровождение кода , особенно если у нас много предикатов и мы хотим сделать их необязательными.
Было бы практическим решением экстернализировать эти предикаты. Со спецификациями JPA мы можем сделать именно это и многое другое.
5. Использование спецификаций JPA
Spring Data представила интерфейс org.springframework.data.jpa.domain.Specification
для инкапсуляции одного предиката:
interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}
Мы можем предоставить методы для создания экземпляров спецификации :
static Specification<Book> hasAuthor(String author) {
return (book, cq, cb) -> cb.equal(book.get("author"), author);
}
static Specification<Book> titleContains(String title) {
return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
}
Чтобы использовать их, нам нужно, чтобы наш репозиторий расширял org.springframework.data.jpa.repository.JpaSpecificationExecutor<T>
:
interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {}
Этот интерфейс объявляет удобные методы для работы со спецификациями . Например, теперь мы можем найти все экземпляры Book
с указанным автором с помощью этой однострочной строки:
bookRepository.findAll(hasAuthor(author));
К сожалению, у нас нет методов, которым мы могли бы передать несколько аргументов спецификации .
Скорее, мы получаем служебные методы в интерфейсе org.springframework.data.jpa.domain.Specification
.
Например, мы можем объединить два экземпляра спецификации
с помощью логического и
:
bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));
В приведенном выше примере where()
— это статический метод класса Specification
.
Таким образом, мы можем сделать наши запросы модульными. Кроме того, нам не нужно было писать шаблон Criteria API, потому что Spring предоставил его нам.
Обратите внимание, что это не означает, что нам больше не придется писать шаблоны критериев; этот подход способен обрабатывать только рабочий процесс, который мы видели, а именно выбор объектов, которые удовлетворяют предоставленным условиям.
Запрос может иметь много структур, которые он не поддерживает, включая группировку, возврат класса, отличного от того, из которого мы выбираем, или подзапросы.
6. Заключение
В этой статье мы обсудили три способа использования запросов критериев в нашем приложении Spring:
- создание класса DAO — самый простой и гибкий способ.
- расширение интерфейса
@Repository
для бесшовной интеграции с автоматическими запросами - использование предикатов в экземплярах
спецификации
, чтобы сделать простые случаи чище и менее подробными
Как обычно, примеры доступны на GitHub .