02.06.2025
5 хвилин читання

Spring Data JPA Specifications: потужний інструмент для динамічних фільтрів. Part 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<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? Задайте своє питання в коментарях нижче! 🤔👇👇

Додати коментар

Ваш імейл не буде опубліковано. Обов'язкові поля відзначені *

Зберегти моє ім'я, імейл та адресу сайту у цьому браузері для майбутніх коментарів