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

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

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

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

API критериев

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

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

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

LocalDate today = new LocalDate();CriteriaBuilder builder = em.getCriteriaBuilder();CriteriaQuery<Movie> query = builder.createQuery(Movie.class);Root<Movie> 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 первый. Кроме того, читабельность кода плохая, потому что трудно быстро определить назначение кода.

Характеристики

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

public MovieSpecifications {

public static Specification<Movie> isComedy() {

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

public static Specification<Movie> isReallyOld() {

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

};
  }
}

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

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

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

public interface MovieRepository extends JpaRepository<Movie, Long>, JpaSpecificationExecutor<Movie> { // query methods here}

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

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

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

Объединить характеристики

Мы можем комбинировать эти отдельные предикаты для удовлетворения бизнес-требований. Для этого используйте and(…) а также or(…) методы объединения атомов Specificationс. Также есть where(…) который обеспечивает некоторый синтаксический сахар, чтобы сделать выражение более читаемым и not(…) что отрицает данную спецификацию. Простой вариант использования выглядит так:

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

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

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

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

Поиск будет осуществляться на основе SearchCriteria объект, который даст нам возможность динамически комбинировать несколько критериев.

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;

}

Теперь SpecificationBuilder может выглядеть так

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

public Specification<Movie> 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;
  }
}

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

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

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

Вывод

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

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *