У попередній частині ми розглянули Spring Data JPA Specifications та створили окремі специфікації для фільтрації користувачів за ім’ям, посадою та навичками. Проте, якщо кожен фільтр потрібно вручну додавати в сервіс, код залишається громіздким. Щоб зробити систему гнучкішою, ми використаємо підхід із SpecificationManager та SpecificationProvider. Він дозволяє автоматично підключати нові специфікації без змін у сервісі та контролері.
Ця концепція базується на патерні “Фабричний метод”, який спрощує керування специфікаціями та дотримується принципу Open/Closed (можливість розширення без змін існуючого коду). У цій частині ми детально розглянемо, як працюють SpecificationManager та SpecificationProvider, як вони взаємодіють із сервісом і контролером, та як легко додавати нові фільтри без зайвих правок у коді.
Фабрика специфікацій у 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);
}
}
Що тут відбувається?
- Створюється мапа (providersMap), де ключ — це назва фільтра (filterKey), а значення — об’єкт відповідної специфікації.
Наприклад:
{
"name": NameSpecificationProvider,
"position": PositionSpecificationProvider,
"skills": SkillSpecificationProvider
}
Тепер, якщо ми передамо “name”, UserSpecificationManager знайде NameSpecificationProvider і поверне відповідну специфікацію.
- Spring автоматично збирає всі SpecificationProvider<User> у список і додає їх у providersMap. Це означає, що нам не потрібно вручну додавати нові специфікації – вони автоматично підключаються до системи.
- Метод get() шукає потрібний SpecificationProvider за ключем (filterKey) і повертає відповідну специфікацію. Якщо такого ключа немає, повертається Specification.where(null), що означає відсутність фільтра.
Чому це зручно?
- Гнучкість – будь-який фільтр можна підключити або прибрати без змін у сервісі чи репозиторії.
- Простота розширення – якщо потрібно додати новий критерій (наприклад, фільтрацію за віком), достатньо просто створити AgeSpecificationProvider, і система автоматично почне його використовувати.
- Код у сервісі залишається чистим, оскільки логіка вибору специфікацій винесена в окремий клас.
Щоб зробити UserSpecificationManager універсальним і чітко визначити його роль, ми винесли логіку управління специфікаціями в окремий інтерфейс SpecificationManager.
Цей інтерфейс дозволяє нам:
- Задати єдину точку доступу до специфікацій.
- Використовувати узагальнений підхід (T може бути будь-якою сутністю, не тільки User).
- Чітко визначити, що будь-який керуючий клас специфікаціями повинен мати метод get().
SpecificationManager відповідає за керування специфікаціями. Його основне завдання — шукати та повертати специфікацію за її ключем (filterKey).
Інтерфейс SpecificationManager виглядає так:
public interface SpecificationManager<T> {
Specification<T> get(String criterion, List<String> params);
}
Як це працює разом?
- Всі специфікації (SpecificationProvider<User>) автоматично збираються у UserSpecificationManager.
- Коли приходить запит із фільтрами, UserServiceImpl звертається до UserSpecificationManager, передаючи filterKey і params.
- UserSpecificationManager шукає специфікацію у своїй мапі та повертає її.
- Ця специфікація додається до загального запиту (Specification.and()).
Приклад роботи 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);
}
}
Що тут відбувається?
- Ініціалізуємо порожню специфікацію (Specification.where(null)).
- Проходимо по всіх вхідних параметрах (filters):
Наприклад, якщо запит GET /users?name=Jonson&position=1,2, то request.entrySet() містить:
{
"name": "Jonson",
"position": "1,2"
}
- Для кожного параметра отримуємо специфікацію через UserSpecificationManager.get().
- name -> NameSpecificationProvider.getSpecification(“Jonson”)
- position -> PositionSpecificationProvider.getSpecification([“1”, “2”])
- Об’єднуємо всі специфікації через .and(sp), тобто всі умови застосовуються одночасно (логіка AND).
- Виконуємо пошук userRepository.findAll(specification, pageable).
Висновки
- SpecificationProvider — це інтерфейс, який дозволяє реалізовувати окремі специфікації для кожного фільтра.
- SpecificationManager — це фабрика, яка керує специфікаціями та дозволяє автоматично отримувати їх за ключем (filterKey).
- Завдяки цьому підходу нам не потрібно змінювати код сервісу при додаванні нових фільтрів.
- Будь-які фільтри можна комбінувати у будь-якому порядку або взагалі не використовувати їх.
Хочете дізнатися більше про Spring Data JPA Specifications? Задайте своє питання в коментарях нижче! 🤔👇👇