В предыдущей части мы рассмотрели 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 в список и добавляет их в providersMap. Это означает, что нам не нужно вручную добавлять новые спецификации — они автоматически подключаются к системе.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? Задайте свой вопрос в комментариях ниже! 🤔👇👇👇