Spring Boot
2026-02-26 20:28 Diff

Выборка списка сущностей в 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>), который отвечает за формирование правильной структуры запроса. Еще с его помощью можно указывать используемые колонки, таблицы, условия фильтрации и сортировки данных

Использование спецификации в контроллере

Что происходит в этом коде:

  1. В метод приходят параметры для фильтрации и страница, которую нужно выбрать
  2. На основе параметров для фильтрации формируется спецификация
  3. Выполняется выборка данных по спецификации и с учетом указанной страницы данных. Из page вычитается единица, потому что иначе получится запрос LIMIT 10 OFFSET 10 вместо LIMIT 10 OFFSET 0
  4. Возвращенный результат Page<Post> преобразуется в Page<PostDTO> с помощью встроенного в Page метода map(), который работает точно так же, как map() в стримах