Определение запросов в Spring Data Common

Весь проект Spring Data Common (и его подпроект Spring Data Jpa) основан на предположении, что при разработке кода доступа к данным большая часть запросов делается только к одной сущности и при этом выполняется фильтрация по значениям полей этой сущности. То есть в основном никаких модных объединений и заковыристых предикатов. А следовательно, код этих запросов достаточно уныл, примитивен и скучен, зато может быть сгенерирован по описанию запроса. И именно этим Spring Data Common и занимается.

Разновидности Repository

Описание запросов начинается с того, что объявляется интерфейс, который расширяет Repository<T,ID extends Serializable>. Наличие Repository в предках класса служит маркером для Spring Data Commons и говорит ему, что надо взять и создать реализацию для методов, декларированных в вашем интерфейсе.При этом репозиторий будет создан для объектов типа T, идентификатором которого будет тип ID. Под идентификатором, как несложно догадаться, выступает то поле класса T, на которое навешена аннотация @Id.

Сам интерфейс Repository не содержит методов и если собственные методы не определить, то результат получится довольно бесполезным. Впрочем, есть уже готовые наследники Repository, которые содержат предопределённые методы.

В первую очередь это CrudRepository<T, ID extends Serializable> который добавляет основные и наиболее нужные методы:

  • count() — который возвращает количество объектов типа T в базе
  • delete() — который удаляет объекты типа T из базы
  • save() — который сохраняет объекты типа T в базу
  • findOne() — который возвращает объект из базы по ID (или null, если вы недостаточно счастливый человек)
  • findAll() — который возвращает все объекты типа T из базы

Потом PagingAndSortingRepository<T, ID extend Serializable> добавляющий методы findAll() с поддержкой сортировки и постраничной выборки. RevisionRepository<T, ID extends Serializable>, добавляющий поддержку ревизий. И, наконец, JpaRepository<T, ID extends Serializable>, который объединяет все вышеперечисленные интерфейсы и их функционал. Самый удобный и популярный интерфейс 🙂

Декларирование запросов

Конечно, можно пользоваться и предопределёнными запросами, но хотелось бы и чего-нибудь поближе к решаемой разработчиком задаче. Запросы определяются по единому шаблону — указывается возвращаемый тип, вид запроса и список предикатов. Например:

  • Country findOneByCodeLike(String code)
  • List<User> findByGroup(Group group)
  • Long countByCurrencyCodeAndAccountType(String code, AccountType type)

Методы запросов могут возвращать следующие типы:

  • T — собственно тот тип, которым вы типизировали интерфейс и на который и настроен репозиторий
  • Option<T>/Optional<T> — то же самое, что и просто T, но вместо null в случае отсутствия объекта вернётся пустой Option<T>/Optional<T>
  • Iterable<T>, Collection<T>, List<T> — на случай, если нужно вернуть более чем один объект
  • Long, Integer — для методов, подсчитывающих количество объектов
  • Stream<T> — на случай, если возвращаемые несколько объектов хочется сразу обработать с использованием потоков
  • Future<T>, Future<List<T>> и так далее — аналог всего вышеперечисленного, но метод не блокируется, а возвращает управление сразу. А данные, очевидно, будут доступны, когда Future завершится

Сам запрос может начинаться с названия операции, которая должна быть выполнена:

  • findOneBy… — Выбирает один объект
  • findBy… — Выбирает несколько объектов
  • countBy… — Подсчитывает количество объектов, которые соответствуют условиям.

И, наконец-то, продолжается всё перечислением предикатов. Каждый предикат описывается названием поля и условием. Например выше findOneByCodeLike найдёт один объект, у которого поле code подобно (sql like) переданному параметру. Параметры передаются в том же порядке, в котором перечисляются предикаты. Самих предикатов существует довольно большое число и все они перечислены в документации на Spring Data Jpa. Я приведу наиболее часто встречающиеся:

  • GreaterThan и GreaterThanEqual — значение должно быть более или более и равно.
  • LessThan и LessThanEqual — значение должно быть менее или менее и равно.
  • IsNull — значение поля должно быть null. При этом передавать параметр не требуется .
  • Like и NotLike — значение должно быть подобно или наоборот, не подобно, параметру. Подобно в терминах SQL like.
  • True и False — значение истинно или ложно. Параметр передавать не требуется.
  • In — значение находится в переданном списке.

Кроме того, в определение запроса можно добавлять сортировку, используя конструкции вида OrderByПолеAsc и OrderByПолеDesc, которые включаются возрастающую или убывающую сортировки по заданному полю. Таких сортировок, очевидно, может быть несколько. А там где есть стабильный порядок, определяемый сортировкой, можно задать и ограничение по количеству возвращаемых записей, используя ключевое слово first. Например List<User> findFirst10ByGroup(Group code)

Недостатки

Понятное дело, пятна бывают даже на солнце и за все хорошее надо расплачиваться. В случае с генерируемым Spring Data Jpa кодом приходится расплачиваться не очень читаемыми названиями методов. В экстремальном случае у вас может быть что-то типа

Вторым недостатком является ограниченность языка запросов — аггрегирующих запросов нет, объединений нет, ничего толком нет. А хуже всего то, что генерация запросов и проверка соответствия имён методов фактическим объектам производится во время исполнения. То есть единственная возможность узнать, что вы опечатались в названии поля, это запустить ваш код и посмотреть как он падает. Ну и на закуску, запросы становятся статичными и сделать, например, динамическую фильтрацию или сменную сортировку не представляется возможным.

К счастью, в Spring Data Jpa есть методы борьбы с этими недостатками, о которых я напишу в следующих статьях.