«1. Введение
Spring Data JPA предоставляет множество способов работы с сущностями, включая методы запросов и пользовательские запросы JPQL. Однако иногда нам нужен более программный подход: например, Criteria API или QueryDSL.
Criteria API предлагает программный способ создания типизированных запросов, что помогает нам избежать синтаксических ошибок. Более того, когда мы используем его с Metamodel API, он проверяет во время компиляции, использовали ли мы правильные имена и типы полей.
Однако у него есть и недостатки: приходится писать многословную логику, раздутую шаблонным кодом.
В этом руководстве мы увидим, как мы можем реализовать нашу пользовательскую логику DAO, используя запросы критериев, и как Spring помогает сократить шаблонный код.
2. Пример приложения
Для простоты в примерах мы реализуем один и тот же запрос несколькими способами: поиск книг по имени автора и названию, содержащему строку.
Объект Book для этого выглядит следующим образом:
@Entity
class Book {
@Id
Long id;
String title;
String author;
// getters and setters
}
Поскольку мы хотим, чтобы все было просто, мы не используем 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\u003cBook\u003e, который описывает, что мы хотим сделать в запросе. Кроме того, он объявляет тип строки в результате. С помощью CriteriaQuery\u003cBook\u003e мы объявляем начальную точку запроса (сущность Book) и сохраняем ее в переменной book для последующего использования. Затем с помощью CriteriaBuilder мы создаем предикаты для нашего Книжная сущность. Обратите внимание, что эти предикаты пока не имеют никакого эффекта. Мы применяем оба предиката к нашему CriteriaQuery. CriteriaQuery.where(Predicate…) объединяет свои аргументы в логическое И. Это момент, когда мы связываем эти предикаты с запросом. После этого мы создаем экземпляр TypedQuery\u003cBook\u003e из нашего 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 и использует его при вызове пользовательских методов.
Допустим, мы хотим выбрать, какие предикаты использовать в нашем запросе. Например, когда мы не хотим находить книги по автору и названию, нам нужно только совпадение автора.
Есть несколько способов сделать это, например, применить предикат только в том случае, если переданный аргумент не равен нулю:
@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\u003cT\u003e:
interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {}
Этот интерфейс объявляет удобные методы для работы со спецификациями. Например, теперь мы можем найти все экземпляры книги с указанным автором с помощью этой однострочной строки:
bookRepository.findAll(hasAuthor(author));
К сожалению, у нас нет методов, которым мы могли бы передать несколько аргументов спецификации. Скорее, мы получаем служебные методы в интерфейсе org.springframework.data.jpa.domain.Specification.
Например, объединение двух экземпляров спецификации с логическим и:
bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));
В приведенном выше примере, где () является статическим методом класса спецификации.
Таким образом, мы можем сделать наши запросы модульными. Кроме того, нам не нужно было писать шаблон Criteria API: Spring предоставил его для нас.
Обратите внимание, что это не означает, что нам больше не нужно писать шаблоны критериев; этот подход способен обрабатывать только рабочий процесс, который мы видели: выбор объектов, которые удовлетворяют предоставленным условиям.
Запрос может иметь много структур, которые он не поддерживает, например, группировка, возврат другого класса, из которого мы выбираем, или подзапросы.
6. Заключение
В этом руководстве мы рассмотрели три способа использования запросов критериев в нашем приложении Spring:
-
создание класса DAO — самый простой и гибкий способ расширения интерфейса @Repository для бесшовной интеграции с автоматические запросы с использованием предикатов в экземплярах спецификации, чтобы сделать простые случаи более понятными и менее подробными
Как обычно, примеры доступны на GitHub.