02.06.2025
5 минут чтения

Spring Data JPA Specifications: мощный инструмент для динамических фильтров. Часть 2

Динамічне фільтрування у Spring Data JPA Ч2

В предыдущей части мы рассмотрели Spring Data JPA Specifications и создали отдельные спецификации для фильтрации пользователей по имени, должности и навыкам. Однако, если каждый фильтр нужно вручную добавлять в сервис, код остается громоздким. Чтобы сделать систему более гибкой, мы используем подход с SpecificationManager и SpecificationProvider. Он позволяет автоматически подключать новые спецификации без изменений в сервисе и контроллере.

Эта концепция базируется на паттерне «Фабричный метод», который упрощает управление спецификациями и придерживается принципа Open/Closed (возможность расширения без изменений существующего кода). В этой части мы подробно рассмотрим, как работают SpecificationManager и SpecificationProvider, как они взаимодействуют с сервисом и контроллером, и как легко добавлять новые фильтры без лишних правок в коде.

Хотите освоить профессию Java Developer? Присоединяйтесь к программе «От 0 до Strong Java Junior за 12 месяцев». Воспользуйтесь выгодным предложением от FoxmindEd!
Зарегистрироваться

Фабрика спецификаций в JPA: динамическое управление фильтрами

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

Как вы заметили, все наши классы спецификаций имплементируют интерфейс SpecificationProvider. Что такое такое и зачем он нужен?

SpecificationProvider — это интерфейс, который реализуется каждой спецификацией. Он позволяет вам:

  • Получать объект спецификации, который можно передать в JpaSpecificationExecutor.
  • Задавать уникальный ключ для каждого фильтра, что позволяет получать нужную спецификацию по имени параметра.
public interface SpecificationProvider<T> {

   Specification<T> getSpecification(List<String> params);

   String getFilterKey();

}

Благодаря этому интерфейсу каждый новый фильтр создается в виде отдельного класса (NameSpecificationProvider, PositionSpecificationProvider и т. д.) без необходимости изменять код сервиса.

UserSpecificationManager — это фабрика, которая содержит карту (Map<String, SpecificationProvider<User>>), где каждому ключу фильтра (name, position, skills) соответствует соответствующая спецификация.

При запуске приложения Spring автоматически находит все спецификации (SpecificationProvider), которые есть в контексте (@Component), и добавляет их в Map<String, SpecificationProvider<User>>.

Вот реализация UserSpecificationManager:

@Component

public class UserSpecificationManager implements SpecificationManager<User> {

   private final Map<String, SpecificationProvider<User>> providersMap;

   public UserSpecificationManager(List<SpecificationProvider<User>> employeeSpecifications) {

       this.providersMap = employeeSpecifications.stream()

                                                 .collect(Collectors.toMap(SpecificationProvider::getFilterKey, identity()));

   }

   @Override

   public Specification<User> get(String filterKey, List<String> params) {

       return providersMap.get(filterKey).getSpecification(params);

   }

}

Что здесь происходит?

  1. Создается карта (providersMap), где ключ — это название фильтра (filterKey), а значение — объект соответствующей спецификации.

Например:

{

  "name": NameSpecificationProvider,

  "position": PositionSpecificationProvider,

  "skills": SkillSpecificationProvider

}

Теперь, если мы передадим «name», UserSpecificationManager найдет NameSpecificationProvider и вернет соответствующую спецификацию.

  1. Spring автоматически собирает все SpecificationProvider в список и добавляет их в providersMap. Это означает, что нам не нужно вручную добавлять новые спецификации — они автоматически подключаются к системе.Spring автоматически собирает все SpecificationProvider<User> в список и добавляет их в providersMap. Это означает, что нам не нужно вручную добавлять новые спецификации — они автоматически подключаются к системе.
  2. Метод get() ищет нужный SpecificationProvider по ключу (filterKey) и возвращает соответствующую спецификацию. Если такого ключа нет, возвращается Specification.where(null), что означает отсутствие фильтра.

Почему это удобно?

  1. Гибкость — любой фильтр можно подключить или убрать без изменений в сервисе или репозитории.
  2. Простота расширения — если нужно добавить новый критерий (например, фильтрацию по возрасту), достаточно просто создать AgeSpecificationProvider, и система автоматически начнет его использовать.
  3. Код в сервисе остается чистым, поскольку логика выбора спецификаций вынесена в отдельный класс.

Чтобы сделать UserSpecificationManager универсальным и четко определить его роль, мы вынесли логику управления спецификациями в отдельный интерфейс SpecificationManager.

Этот интерфейс позволяет нам:

  • Задать единую точку доступа к спецификациям.
  • Использовать обобщенный подход (T может быть любой сущностью, не только User).
  • Четко определить, что любой управляющий класс спецификациями должен иметь метод get().

SpecificationManager отвечает за управление спецификациями. Его основная задача — искать и возвращать спецификацию по ее ключу (filterKey).

Интерфейс SpecificationManager выглядит так:

public interface SpecificationManager<T> {

   Specification<T> get(String criterion, List<String> params);

}

Как это работает вместе?

  1. Все спецификации (SpecificationProvider<User>) автоматически собираются в UserSpecificationManager.
  2. Когда приходит запрос с фильтрами, UserServiceImpl обращается к UserSpecificationManager, передавая filterKey и params.
  3. UserSpecificationManager ищет спецификацию в своей карте и возвращает ее.
  4. Эта спецификация добавляется к общему запросу (Specification.and()).
Подпишитесь на наш Ютуб-канал! Полезные видео для программистов уже ждут вас! YouTube
Выберите свой курс! Путь к карьере программиста начинается здесь! Посмотреть

Робот приложения UserSpecificationManager в UserServiceImpl

В сервисе мы просто проходим по всем параметрам фильтрации (filters) и вызываем соответствующую спецификацию через UserSpecificationManager.

@Service

@AllArgsConstructor

public class UserServiceImpl implements UserService {

   private final UserSpecificationManager userSpecificationManager;

   private final UserRepository userRepository;

   private static final String SORT_ORDER = "sortOrder";

   private static final String PAGE = "page";

   private static final String SIZE = "size";

   private static final String SORT_BY = "sortBy";

   private static final String DEFAULT_SORT_PARAMETER = "lastName";

   private static final String DEFAULT_PAGE_NUMBER = "0";

   private static final String DEFAULT_PAGE_SIZE = "5";

   private static final String DEFAULT_SORT_ORDER = "asc";

   /*

    * This is a learning project to demonstrate the use of JPA Specifications.

    * We are not validating input parameters, handling errors, or using DTOs for response transformation.

    * In a real-world application, proper validation, error handling, and DTO mapping should be implemented.

    */

   @Override

   public Page<User> filterUsers(Map<String, String> request) {

       Specification<User> specification = Specification.where(null);

       Pageable pageable = getPageable(request);

       for (Map.Entry<String, String> entry : request.entrySet()) {

           Specification<User> sp = userSpecificationManager.get

                   (entry.getKey(), List.of(entry.getValue().split(",")));

           specification = specification.and(sp);

       }

       return userRepository.findAll(specification, pageable);

   }

   private Pageable getPageable(Map<String, String> request) {

       Sort.Direction orderingDirection = Sort.Direction.fromString(request.get(SORT_ORDER) == null ?

               DEFAULT_SORT_ORDER : request.remove(SORT_ORDER));

       Sort sortByRequest = Sort.by(orderingDirection, request.get(SORT_BY) == null ?

               DEFAULT_SORT_PARAMETER : request.remove(SORT_BY));

       return PageRequest.of(Integer.parseInt(request.get(PAGE) == null ?

                       DEFAULT_PAGE_NUMBER : request.remove(PAGE)),

               Integer.parseInt(request.get(SIZE) == null ? DEFAULT_PAGE_SIZE : request.remove(SIZE)), sortByRequest);

   }

}

Что здесь происходит?

  1. Инициализируем пустую спецификацию (Specification.where(null)).
  2. Проходим по всем входным параметрам (filters):

Например, если запрос GET /users?name=Jonson&position=1,2, то request.entrySet() содержит:

{

  "name": "Jonson",

  "position": "1,2"

}

  1. Для каждого параметра получаем спецификацию через UserSpecificationManager.get().
    • name -> NameSpecificationProvider.getSpecification(«Jonson»)
    • position -> PositionSpecificationProvider.getSpecification([«1», «2»])
  2. Объединяем все спецификации через .and(sp), то есть все условия применяются одновременно (логика AND).
  3. Выполняем поиск userRepository.findAll(specification, pageable).

Выводы

  • SpecificationProvider — это интерфейс, который позволяет реализовывать отдельные спецификации для каждого фильтра.
  • SpecificationManager — это фабрика, которая управляет спецификациями и позволяет автоматически получать их по ключу (filterKey).
  • Благодаря этому подходу нам не нужно менять код сервиса при добавлении новых фильтров.
  • Любые фильтры можно комбинировать в любом порядке или вообще не использовать их.
FAQ
SpecificationProvider позволяет отделить каждый фильтр в свой класс и автоматически подключать его без изменений в сервисе. Это уменьшает связность кода, придерживается принципа Open/Closed и упрощает расширение.
UserSpecificationManager хранит все спецификации в карте, где ключ - это название фильтра (filterKey). Spring автоматически подтягивает все реализации SpecificationProvider в контекст и добавляет их в эту карту. После этого get() метод возвращает соответствующую спецификацию по ключу.
Достаточно создать новую реализацию SpecificationProvider (например, AgeSpecificationProvider) и пометить ее как @Component. Spring автоматически зарегистрирует ее в UserSpecificationManager, и сервис начнет использовать ее без изменений в коде.
Если UserSpecificationManager не найдет соответствующий SpecificationProvider для переданного ключа, то метод get() вернет Specification.where(null), что эквивалентно отсутствию фильтрации по этому параметру.
Вся логика выбора, сбора и регистрации спецификаций вынесена из сервисного слоя. В сервисе остается только одна ответственность - передать параметры и объединить полученные спецификации. Это упрощает тестирование и поддержку.
Да. SpecificationManager - это универсальный интерфейс, и его можно реализовать для любой сущности (например, ProductSpecificationManager, OrderSpecificationManager), что делает решение масштабируемым и повторно используемым.

Хотите узнать больше о Spring Data JPA Specifications? Задайте свой вопрос в комментариях ниже! 🤔👇👇👇

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

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

Сохранить моё имя, имейл и адрес сайта в этом браузере для будущих комментариев