«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.