Spring Boot
Теория: Поиск
Выборка списка сущностей в API почти всегда подразумевает какую-то фильтрацию данных. Например, список постов конкретного автора, за какой-то срок или только опубликованных. А если данных много даже после фильтрации, то они отдаются постранично. Реализовать необходимую логику можно с помощью:
- Автоматической генерации методов в JPA Repository в простых случаях
- JPA Specifications в более сложных случаях
- QueryDSL или других сторонних библиотек
В этом уроке мы поговорим о JPA Specifications — механизме, который позволяет динамически собирать сложные запросы в рамках одного метода без необходимости создавать новый метод под каждое условие выборки.
JPA Specifications
Для реализации этого механизма нужно выполнить следующие шаги:
- Добавить интерфейс
JpaSpecificationExecutorв репозиторий - Создать DTO для параметров запроса, который будет использоваться для фильтрации
- Описать спецификацию для конкретной сущности
- Внедрить использование спецификации в контроллере
Все это мы будем добавлять для сущности Post. Ее код выглядит так:
Обновление репозитория
Для работы динамического фильтра на базе спецификации нужно добавить интерфейс JpaSpecificationExecutor. В нем описаны методы для работы с данными на основе спецификации:
Метод findAll интерфейса JpaSpecificationExecutor возвращает страницу с постами на основе переданной спецификации. Вторым параметром метод принимает Pageable, который определяет смещение и количество данных в части LIMIT. Это хорошая практика, потому что возвращение всех данных почти всегда приводит к проблемам с производительностью:
Создание DTO
Обычно фильтры состоят больше, чем из одного параметра. В этом случае неудобно получать каждый параметр по отдельности. Гораздо проще создать для них DTO, который будет создан при вызове метода контроллера. Spring Boot автоматически сопоставляет параметры запроса со свойствами объекта и заполняет их, если они переданы:
Сам DTO включает те параметры, по которым мы хотим фильтровать. В нашем случае это будет:
- Параметр
authorIdвыбирает посты по автору - Параметр
nameContвыбирает посты по вхождению в название поста (здесь Cont обозначает contain — «содержать») - Параметр
createdAtGtвыбирает посты, появившиеся позже указанной даты (здесь gt обозначает greater than — «более чем») - Параметр
createdAtLtон выбирает посты, появившиеся раньше указанной даты (здесь lt обозначает lesser than — «менее чем»)
Добавлять суффиксы Cont, Gt и Lt в название полей не обязательно. С другой стороны, это очень удобно, потому что позволяет использовать одно и то же поле несколько раз так, что сразу понятно, для чего нужен этот параметр и как он примерно работает. Ниже код соответствующего DTO:
Создание спецификации
Спецификация работает как билдер, которому передаются различные условия фильтрации. На базе этой спецификации Spring Boot JPA выполняет генерацию SQL. Ниже один из примеров описания спецификации:
В методе build происходит сборка спецификации на основе переданных параметров. Каждый параметр формирует свое условие фильтрации данных. Обработка каждого параметра вынесена в свой метод для удобства. Внутри этих методов есть общая логика, связанная с проверкой наличия параметра. Если он отсутствует, то возвращается cb.conjunction(), который ни на что не влияет, но нужен для работы цепочки методов.
Спецификация представляет собой лямбда-функцию с тремя параметрами:
- Объект
root(Root<T>), который считается представлением сущности. С помощью него мы указываем, по какому свойству нужно выполнять фильтрацию, включая обращение к свойствам зависимых сущностей - Объект
cb(CriteriaBuilder), который предоставляет методы для создания фильтров —equal(),like()иgreaterThan() - Объект
query(CriteriaQuery<T>), который отвечает за формирование правильной структуры запроса. Еще с его помощью можно указывать используемые колонки, таблицы, условия фильтрации и сортировки данных
Использование спецификации в контроллере
Что происходит в этом коде:
- В метод приходят параметры для фильтрации и страница, которую нужно выбрать
- На основе параметров для фильтрации формируется спецификация
- Выполняется выборка данных по спецификации и с учетом указанной страницы данных. Из
pageвычитается единица, потому что иначе получится запросLIMIT 10 OFFSET 10вместоLIMIT 10 OFFSET 0 - Возвращенный результат
Page<Post>преобразуется вPage<PostDTO>с помощью встроенного вPageметодаmap(), который работает точно так же, какmap()в стримах



