Используйте критерии критериев в приложении данных Spring

Используйте критерии критериев в приложении данных Spring

1. Вступление

Spring Data JPA предоставляет множество способов работы с объектами, включаяquery methods и пользовательскийJPQL queries. Однако иногда нам нужен более программный подход: например,Criteria API илиQueryDSL.

Criteria API offers a programmatic way to create typed queries, что помогает нам избежать синтаксических ошибок. Более того, когда мы используем его с Metamodel API, он выполняет проверку во время компиляции, использовали ли мы правильные имена и типы полей.

Однако, у этого есть свои недостатки: мы должны написать многословную логику, раздутую с помощью стандартного кода.

В этом руководстве мы увидим, как реализовать нашу настраиваемую логику DAO с помощью критериальных запросов и как Spring помогает сократить шаблонный код.

2. Образец заявки

Для простоты в примерах мы реализуем один и тот же запрос несколькими способами: поиск книг по имени автора и названию, содержащемуString.

СущностьBook для этого выглядит так:

@Entity
class Book {

    @Id
    Long id;
    String title;
    String author;

    // getters and setters

}

Поскольку мы хотим, чтобы все было просто, мы не используем Metamodel API в этом руководстве.

3. @Repository Класс

Как известно, в модели компонента Springwe should place our data access logic in @Repository beans. Конечно, эта логика может использовать любую реализацию, например, Criteria API.

Для этого нам нужен только экземплярEntityManager, который мы можем подключить автоматически:

@Repository
class BookDao {

    EntityManager em;

    // constructor

    List findBooksByAuthorNameAndTitle(String authorName, String title) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery cq = cb.createQuery(Book.class);

        Root 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 query = em.createQuery(cq);
        return query.getResultList();
    }

}

Приведенный выше код соответствует стандартному рабочему процессу Criteria API:

  • Сначала мы получаем ссылкуCriteriaBuilder, которую мы можем использовать для создания различных частей запроса.

  • ИспользуяCriteriaBuilder, мы создаемCriteriaQuery<Book>, который описывает, что мы хотим сделать в запросе. Кроме того, он объявляет тип строки в результате

  • С помощьюCriteriaQuery<Book> мы объявляем начальную точку запроса (сущностьBook) и сохраняем ее в переменнойbook для дальнейшего использования.

  • Затем с помощьюCriteriaBuilder мы создаем предикаты для нашей сущностиBook. Обратите внимание, что эти предикаты пока не действуют.

  • Мы применяем оба предиката к нашемуCriteriaQuery..CriteriaQuery.where(Predicate…) объединяет свои аргументы в логическийand. Это тот момент, когда мы связываем эти предикаты с запросом

  • После этого мы создаем экземплярTypedQuery<Book> из нашегоCriteriaQuery

  • Наконец, мы возвращаем все подходящие объектыBook

Обратите внимание: поскольку мы отметили класс DAO с помощью@Repository, тоSpring enables exception translation для этого класса.

4. Расширение хранилища с помощью пользовательских методов

Наличиеautomatic custom queries - это мощная функция Spring Data. Однако иногда нам нужна более сложная логика, которую невозможно создать с помощью автоматических методов запроса.

Мы можем реализовать эти запросы в отдельных классах DAO (как в предыдущем разделе).

Кроме того,if we want a @Repository interface to have a method with a custom implementation, we can use composable repositories.

Пользовательский интерфейс выглядит так:

interface BookRepositoryCustom {
    List findBooksByAuthorNameAndTitle(String authorName, String title);
}

И интерфейс@Repository:

interface BookRepository extends JpaRepository, BookRepositoryCustom {}

Кроме того, мы должны изменить наш предыдущий класс DAO для реализацииBookRepositoryCustom и переименовать его вBookRepositoryImpl:

@Repository
class BookRepositoryImpl implements BookRepositoryCustom {

    EntityManager em;

    // constructor

    @Override
    List findBooksByAuthorNameAndTitle(String authorName, String title) {
        // implementation
    }

}

Когда мы объявляемBookRepository как зависимость, Spring находитBookRepositoryImpl и использует его, когда мы вызываем пользовательские методы.

Допустим, мы хотим выбрать, какие предикаты использовать в нашем запросе. Например, если мы не хотим искать книги по автору и названию, нам нужно только сопоставить автора.

Есть несколько способов сделать это, например, применить предикат, только если переданный аргумент неnull:

@Override
List findBooksByAuthorNameAndTitle(String authorName, String title) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery cq = cb.createQuery(Book.class);

    Root book = cq.from(Book.class);
    List 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();
}

Однако этот подходmakes the code hard to maintain, особенно если у нас много предикатов и мы хотим сделать их необязательными.

Было бы практическим решением воплотить эти предикаты во внешний вид. С помощью спецификаций JPA мы можем сделать именно это; и даже больше.

5. Использование спецификаций JPA

Spring Data представила интерфейсorg.springframework.data.jpa.domain.Specification для инкапсуляции одного предиката:

interface Specification {
    Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb);
}

Мы можем предоставить методы для создания экземпляровSpecification:

static Specification hasAuthor(String author) {
    return (book, cq, cb) -> cb.equal(book.get("author"), author);
}

static Specification titleContains(String title) {
    return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
}

Чтобы использовать их, нам нужно, чтобы наш репозиторий расширилorg.springframework.data.jpa.repository.JpaSpecificationExecutor<T>:

interface BookRepository extends JpaRepository, JpaSpecificationExecutor {}

Это интерфейсdeclares handy methods to work with specifications. Например, теперь мы можем найти все экземплярыBook с указанным автором с помощью этого однострочника:

bookRepository.findAll(hasAuthor(author));

К сожалению, у нас нет методов, которым можно было бы передать несколько аргументовSpecification. Скорее мы получаем служебные методы в интерфейсеorg.springframework.data.jpa.domain.Specification.

Например, объединение двух экземпляровSpecification с логическим и:

bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));

В приведенном выше примереwhere() - статический метод классаSpecification.

Таким образом мы можем сделать наши запросы модульными. Кроме того, нам не нужно было писать шаблон Criteria API: Spring предоставил его для нас.

Обратите внимание: это не означает, что нам больше не нужно писать шаблон критериев; этот подход способен обрабатывать только рабочий процесс, который мы видели: выбор сущностей, которые удовлетворяют предоставленным условиям.

Запрос может иметь множество структур, которые он не поддерживает, например группирование, возврат другого класса, из которого мы выбираем, или подзапросы.

6. Заключение

В этом руководстве мы увидели три способа использования критериальных запросов в нашем приложении Spring:

  • создание класса DAO - самый простой и гибкий способ

  • расширение интерфейса@Repository для полной интеграции с автоматическими запросами

  • использование предикатов в экземплярахSpecification, чтобы сделать простые случаи более понятными и менее подробными

Как обычно доступны примерыover on GitHub.