Несмотря на то, что Querydsl успешно решает проблему с динамическими и типобезопасными запросами в Spring Data Commons, есть причины и не использовать его: зависимость от дополнительной библиотеки, необходимость генерации вспомогательного кода, не совсем удобный (на мой взгляд) подход с дополнительными классами для описания условий.
Однако в Spring Data Commons есть ещё несколько различных механизмов для формирования динамических запросов. Один из них, это, известный ещё по Hibernate, запрос по примеру (query by example), использующий частично заполненную данными сущность как пример того, что нужно получить из базы данных.
Код примеров ниже основан на коде из статьи Hello, Spring Data JPA
Интерфейс QueryByExampleExecutor
Чтобы включить поддержку запросов по примеру в генерируемый репозиторий, необходимо чтоб он имел в списке предков интерфейс QueryByExampleExecutor<T>, где T — тип сущности, с которой работает репозиторий. При этом, даже если планируется работать только с запросами по примеру, наличие в предках интерфейса Repository или любого из его наследников всё равно является обязательным.
1
|
public interface PassportRepository extends CrudRepository<Passport, Long>, QueryByExampleExecutor<Passport> { }
|
1
|
public interface PersonRepository extends CrudRepository<Person, Long>, QueryByExampleExecutor<Person> { }
|
Интерфейс QueryByExampleExecutor<T> определяет несколько методов:
- T FindOne(Example) — возвращает один объект, соответствующий условия
- Iterable<T> findAll(Example) — возвращает несколько объектов, соответствующих условию. Обратите внимание, что возвращается всегда Iterable<T>, без возможности уточнить тип
- long count(Example) — возвращает количество объектов в базе данных, соответствующих условию
- boolean exists(Example) — сообщает, есть ли в базе данных объект соответствующий условию
Все методы принимают объект класса Example, содержащего в себе условия запроса.
Простой запрос по примеру
1
2
3
4
5
6
7
|
Person queryPerson = new Person();
queryPerson.setFirstName("Test");
Example<Person> exampleQuery = Example.of(queryPerson);
personRepository.findAll(exampleQuery)
.forEach(System.out::println);
|
Для чего нужен класс Example?
Для того, чтобы задавать условия над значениями. Если использовать только сами сущности напрямую, то можно было бы задавать только условие равенства, как в примере выше: «поле firstName имеет значение Test». Все остальные условия, такие как «больше», «меньше», «начинается с..» и так далее просто недоступны: в классе сущности нет механизмов для применения подобных операторов, поэтому приходится прикладывать их извне. И именно для этого сущность оборачивается в объект класса Example.
1
2
3
4
5
|
ExampleMatcher firstNameMatcher = ExampleMatcher.matching()
.withMatcher("firstname", m -> m.startsWith());
queryPerson.setFirstName("fail");
Example<Person> failingQuery = Example.of(queryPerson, firstNameMatcher);
assertFalse(personRepository.exists(failingQuery));
|
Логические операторы, прикладываемые к значениям, описываются с помощью отдельного вспомогательного класса ExampleMatcher, объект которого передаётся в вызов Example.of() вместе с сущностью. К сожалению, добавление операторов не типобезопасно и проверяется только во время исполнения, что является существенным неудобством.
Ограничения
Запросы по примеру вещь весьма простая и удобная и из её простоты вытекают её недостатки:
- Допустимо только одно значение на поле. Невозможно, например, построить запрос аналогичный «firstName = ‘TEST or firstName = ‘Testoff'»
- Для строк поддерживаются регулярные выражения и матчеры, из примера выше, для остальных типов только простые логические операции
- Возможность делать запросы по значениям вложенных объектов весьма ограничена
Код примера доступен на github.