Рубрики
Без рубрики

Весна: Расширенный поиск и фильтрация

Создание сложных запросов поверх шаблона репозитория

Автор оригинала: Milan.

Реализация уровня доступа к данным в приложении довольно долгое время была громоздкой. Слишком много шаблонного кода должно быть написано для выполнения простых запросов, а также для разбиения на страницы и аудита.

Spring Data JPA был создан в первую очередь для улучшения реализации уровня доступа к данным. Это уменьшает объем стандартного кода, требуемого JPA, что упрощает и ускоряет реализацию уровня сохраняемости. JPA Spring Data предоставляет реализацию по умолчанию для каждого метода, определенного одним из его интерфейсов репозитория. Это означает, что вам больше не нужно выполнять базовые операции чтения или записи. Кроме того, Spring Data JPA обеспечивает генерацию запросов к базе данных на основе имен методов (если запрос не слишком сложный).

public interface MovieRepository extends JpaRepository { List findByTitle(String title, Sort sort); Page findByYear(Int year, Pageable pageable);}

API критериев

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

API критериев предлагает программный способ создания типизированных запросов, который помогает нам избежать синтаксических ошибок. Более того, когда мы используем его с API метамодели, он проверяет во время компиляции, правильно ли мы использовали имена и типы полей.

Неофициально предикат-это утверждение, которое может быть истинным или ложным в зависимости от значений его переменных. Java Интерфейс предиката – это функциональный интерфейс, который часто используется в качестве цели назначения для лямбда-выражений.

LocalDate today = new LocalDate();CriteriaBuilder builder = em.getCriteriaBuilder();CriteriaQuery query = builder.createQuery(Movie.class);Root root = query.from(Movie.class);Predicate isComedy = builder.equal(root.get(Movie.genre), Genre.Comedy);Predicate isReallyOld = builder.lessThan(root.get(Movie.createdAt), today.minusYears(25));

query.where(builder.and(isComedy, isReallyOld));em.createQuery(query.select(root)).getResultList();

Основная проблема этого подхода заключается в том, что предикаты нелегко экстернализировать и повторно использовать , поскольку сначала необходимо настроить CriteriaBuilder, CriteriaQuery и Root . Кроме того, читаемость кода низкая, потому что трудно быстро определить цель кода.

Технические характеристики

Чтобы иметь возможность определять многоразовые Предикат s мы собираемся изучить Спецификацию интерфейс. Он определяет спецификацию как предикат над сущностью, что еще больше упрощает реализацию уровня доступа к данным.

public MovieSpecifications {

public static Specification isComedy() {

return (root, query, cb) -> {
         return cb.equal(root.get(Movie_.genre), Genre.Comedy);
     };
  }

public static Specification isReallyOld() {

return (root, query, cb) -> {
        return cb.lessThan(root.get(Movie_.createdAt), new LocalDate.now().minusYears(25));

};
  }
}

Мы только что создали многоразовые предикаты, которые могут быть выполнены индивидуально. Это не самый красивый код в мире, но он служит своей цели. Чтобы сделать это еще более чистым, каждую спецификацию можно смоделировать как отдельную спецификацию.

public MovieComedySpecification implements Specification {
  @Override
  public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
    return cb.equal(root.get(Movie_.genre), Genre.Comedy);
}

Следующий вопрос: как мы будем выполнять эти спецификации? Для этого просто расширьте JpaSpecificationExecutor в интерфейсе репозитория и, таким образом, “втяните” API для выполнения Спецификации с:

public interface MovieRepository extends JpaRepository, JpaSpecificationExecutor { // query methods here}

Теперь клиент может сделать:

movieRepository.findAll(MovieSpecifications.isComedy());movieRepository.findAll(MovieSpecifications.isReallyOld());

Здесь базовая реализация репозитория подготовит Запрос критериев , Root и CriteriaBuilder , примените Предикат , созданный данной Спецификацией , и выполните запрос. Но разве мы не могли просто создать простые методы запросов для достижения этой цели?

Технические характеристики комбайна

Мы можем объединить эти отдельные предикаты в соответствии с бизнес-требованиями. Для этого используйте и(...) и или(...) методы объединения атомарных Спецификация s. Есть также где(...) это обеспечивает некоторый синтаксический сахар, чтобы сделать выражение более читабельным, и не (...) , что отрицает данную спецификацию. Простой вариант использования выглядит так:

movieRepository.findAll(Specification.where(MovieSpecifications.isComedy()) .and(MovieSpecifications.isReallyOld()));

Это улучшает читабельность, а также обеспечивает дополнительную гибкость по сравнению с использованием только API критериев. Единственным недостатком здесь является то, что разработка Спецификации реализации требует немалых усилий по кодированию.

Конструктор спецификаций

Если бизнес-правила поиска заложены в камне, описанная выше реализация может служить своей цели, но если у нас есть динамические запросы с сочетанием нескольких ограничений, которые больше не будут работать легко. Чтобы решить эту проблему сочетания нескольких спецификаций, мы можем ввести Конструктор спецификаций.

Поиск будет основан на объекте Критерии поиска , что даст нам возможность динамически комбинировать несколько критериев.

public enum SearchOperation {                           
  EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE;

public static final String[] SIMPLE_OPERATION_SET = 
   { ":", "!", ">", "<", "~" };

public static SearchOperation getSimpleOperation(final char input)
  {
    switch (input) {
      case ':': return EQUALITY;
      case '!': return NEGATION;
      case '>': return GREATER_THAN;
      case '<': return LESS_THAN;
      case '~': return LIKE;
      default: return null;
    }
  }
}

public class SearchCriteria {

private String key; private Object value; private SearchOperation operation;

}

Теперь конструктор спецификаций может выглядеть следующим образом

public final class MovieSpecificationsBuilder {
  private final List params;
  
  public MovieSpecificationsBuilder() {
    params = new ArrayList<>();
  }

public Specification build() { 
    // convert each of SearchCriteria params to Specification and construct combined specification based on custom rules
  }

public final MovieSpecificationsBuilder with(final SearchCriteria criteria) { 
    params.add(criteria);
    return this;
  }
}

Movie SpecificationBuilder теперь отвечает за создание спецификации из нескольких критериев поискового запроса на основе определенных бизнес-правил. Остальная часть кода вообще не нуждается в изменении! Теперь клиент может указать критерии и получить результат, выполнив следующие действия:

final MovieSpecificationsBuilder msb = new MovieSpecificationsBuilder();
// add SearchCriteria by invoking with()

final Specification spec = msb.build();
movieRepository.findAll(spec);

Вывод

В этой статье рассматривается простая реализация, которая может стать основой мощного языка запросов REST. Используя спецификации данных Spring, код может быть более чистым и гибким для поддержки пользовательских запросов пользователей. Кроме того, добавление новых критериев поиска с Спецификацией s теперь становится тривиальной задачей.

Оригинал: “https://www.codementor.io/@milanflink/spring-advanced-search-filtering-utxf2wdm3”