1. Обзор
Spring MVC и Spring Data сами по себе отлично справляются с задачей по упрощению разработки приложений. Но что, если мы сложим их вместе?
В этом руководстве мы рассмотрим веб-поддержку Spring Data и то, как ее распознаватели
могут уменьшить количество шаблонов и сделать наши контроллеры более выразительными.
Попутно мы взглянем на Querydsl и на то, как выглядит его интеграция со Spring Data.
2. Немного предыстории
Веб-поддержка Spring Data — это набор веб-функций, реализованных поверх стандартной платформы Spring MVC и направленных на добавление дополнительных функций на уровень контроллера .
Функциональность веб-поддержки Spring Data построена вокруг нескольких классов преобразователя .
Резолверы упрощают реализацию методов контроллера, которые взаимодействуют с репозиториями данных Spring , а также обогащают их дополнительными функциями.
Эти функции включают выборку объектов предметной области из уровня репозитория без явного вызова реализаций репозитория и создание ответов контроллера , которые могут быть отправлены клиентам в виде сегментов данных, поддерживающих разбиение на страницы и сортировку.
Кроме того, запросы к методам контроллера, которые принимают один или несколько параметров запроса, могут быть внутренне разрешены в запросы Querydsl .
3. Демонстрационный проект Spring Boot
Чтобы понять, как мы можем использовать веб-поддержку Spring Data для улучшения функциональности наших контроллеров, давайте создадим базовый проект Spring Boot.
Зависимости Maven нашего демонстрационного проекта довольно стандартны, за некоторыми исключениями, которые мы обсудим позже:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
В этом случае мы включили spring-boot-starter-web
, так как мы будем использовать его для создания контроллера RESTful, spring-boot-starter-jpa
для реализации уровня сохраняемости и spring-boot-starter-test
для тестирования API контроллера.
Поскольку мы будем использовать H2 в качестве базовой базы данных, мы также включили com.h2database
.
Имейте в виду, что spring-boot-starter-web
по умолчанию включает веб-поддержку Spring Data. Следовательно, нам не нужно создавать какие-либо дополнительные классы @Configuration
, чтобы заставить его работать в нашем приложении.
И наоборот, для проектов, отличных от Spring Boot, нам нужно определить класс @Configuration
и аннотировать его аннотациями @EnableWebMvc
и @EnableSpringDataWebSupport
.
3.1. Класс домена
Теперь давайте добавим в проект простой класс сущностей User
JPA, чтобы у нас была работающая модель предметной области для экспериментов:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private final String name;
// standard constructor / getters / toString
}
3.2. Слой репозитория
Чтобы не усложнять код, функциональность нашего демонстрационного приложения Spring Boot будет сужена до простого извлечения некоторых сущностей пользователя
из базы данных H2 в памяти.
Spring Boot упрощает создание реализаций репозитория, предоставляющих минимальную функциональность CRUD «из коробки». Поэтому давайте определим простой интерфейс репозитория, который работает с сущностями User
JPA:
@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {}
В определении интерфейса UserRepository
нет ничего сложного , за исключением того, что он расширяет PagingAndSortingRepository
.
Это сигнализирует Spring MVC о необходимости автоматического разбиения по страницам и сортировки записей базы данных .
3.3. Уровень контроллера
Теперь нам нужно реализовать хотя бы базовый контроллер RESTful, который действует как средний уровень между клиентом и уровнем репозитория.
Поэтому давайте создадим класс контроллера, который берет экземпляр UserRepository
в свой конструктор и добавляет единственный метод для поиска сущностей пользователя по
id
:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User findUserById(@PathVariable("id") User user) {
return user;
}
}
3.4. Запуск приложения
Наконец, давайте определим основной класс приложения и заполним базу данных H2 несколькими объектами User :
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
CommandLineRunner initialize(UserRepository userRepository) {
return args -> {
Stream.of("John", "Robert", "Nataly", "Helen", "Mary").forEach(name -> {
User user = new User(name);
userRepository.save(user);
});
userRepository.findAll().forEach(System.out::println);
};
}
}
Теперь давайте запустим приложение. Как и ожидалось, мы видим список постоянных пользовательских
сущностей, выводимых на консоль при запуске:
User{id=1, name=John}
User{id=2, name=Robert}
User{id=3, name=Nataly}
User{id=4, name=Helen}
User{id=5, name=Mary}
4. Класс DomainClassConverter
На данный момент класс UserController реализует только метод
findUserById()
.
На первый взгляд реализация метода выглядит достаточно просто. Но на самом деле он инкапсулирует множество функций веб-поддержки Spring Data за кулисами.
Поскольку метод принимает экземпляр User
в качестве аргумента, мы можем в конечном итоге подумать, что нам нужно явно передать объект домена в запросе. Но мы этого не делаем.
Spring MVC использует класс DomainClassConverter
для преобразования переменной пути id в тип
id
класса домена и использует его для извлечения соответствующего объекта домена из слоя репозитория . Дальнейший поиск не требуется.
Например, HTTP-запрос GET к конечной точке http://localhost:8080/users/1
вернет следующий результат:
{
"id":1,
"name":"John"
}
Следовательно, мы можем создать интеграционный тест и проверить поведение метода findUserById()
:
@Test
public void whenGetRequestToUsersEndPointWithIdPathVariable_thenCorrectResponse() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/users/{id}", "1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"));
}
}
В качестве альтернативы мы можем использовать инструмент тестирования REST API, такой как Postman , для проверки метода.
Преимущество DomainClassConverter
в том, что нам не нужно явно вызывать реализацию репозитория в методе контроллера.
Просто указав переменную пути id
вместе с разрешимым экземпляром класса домена, мы автоматически инициировали поиск объекта домена .
5. Класс PageableHandlerMethodArgumentResolver
Spring MVC поддерживает использование типов Pageable
в контроллерах и репозиториях.
Проще говоря, экземпляр Pageable
— это объект, который содержит информацию о подкачке. Поэтому, когда мы передаем аргумент Pageable
методу контроллера, Spring MVC использует класс PageableHandlerMethodArgumentResolver
для преобразования
экземпляра Pageable в объект PageRequest
, который является простой реализацией Pageable
.
5.1. Использование Pageable
в качестве параметра метода контроллера
Чтобы понять, как работает класс PageableHandlerMethodArgumentResolver
, давайте добавим новый метод в класс UserController :
@GetMapping("/users")
public Page<User> findAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
В отличие от метода findUserById()
, здесь нам нужно вызвать реализацию репозитория, чтобы получить все объекты User
JPA, сохраненные в базе данных.
Поскольку метод принимает экземпляр Pageable
, он возвращает подмножество всего набора сущностей, хранящихся в объекте Page<User> .
Объект Page
— это подсписок списка объектов, который предоставляет несколько методов, которые мы можем использовать для получения информации о выгруженных результатах , включая общее количество страниц результатов и номер страницы, которую мы извлекаем.
По умолчанию Spring MVC использует класс PageableHandlerMethodArgumentResolver для создания объекта
PageRequest
со следующими параметрами запроса:
page
: индекс страницы, которую мы хотим получить — параметр нулевой индекс и его значение по умолчанию равно0
size
: количество страниц, которые мы хотим получить — значение по умолчанию —20 .
sort
: одно или несколько свойств, которые мы можем использовать для сортировки результатов, используя следующий формат:свойство1, свойство2(,asc|desc) —
например,?sort=name&sort=email,asc
Например, запрос GET к конечной точке http://localhost:8080/user вернет
следующий результат:
{
"content":[
{
"id":1,
"name":"John"
},
{
"id":2,
"name":"Robert"
},
{
"id":3,
"name":"Nataly"
},
{
"id":4,
"name":"Helen"
},
{
"id":5,
"name":"Mary"
}],
"pageable":{
"sort":{
"sorted":false,
"unsorted":true,
"empty":true
},
"pageSize":5,
"pageNumber":0,
"offset":0,
"unpaged":false,
"paged":true
},
"last":true,
"totalElements":5,
"totalPages":1,
"numberOfElements":5,
"first":true,
"size":5,
"number":0,
"sort":{
"sorted":false,
"unsorted":true,
"empty":true
},
"empty":false
}
Как мы видим, ответ включает в себя элементы JSON first
, pageSize
, totalElements
и totalPages
. Это действительно полезно, так как внешний интерфейс может использовать эти элементы для простого создания механизма подкачки.
Кроме того, мы можем использовать интеграционный тест для проверки метода findAllUsers()
:
@Test
public void whenGetRequestToUsersEndPoint_thenCorrectResponse() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/users")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$['pageable']['paged']").value("true"));
}
5.2. Настройка параметров пейджинга
Во многих случаях нам потребуется настроить параметры разбиения по страницам. Самый простой способ сделать это — использовать аннотацию @PageableDefault
:
@GetMapping("/users")
public Page<User> findAllUsers(@PageableDefault(value = 2, page = 0) Pageable pageable) {
return userRepository.findAll(pageable);
}
В качестве альтернативы мы можем использовать статический фабричный метод PageRequest
of()
для создания пользовательского объекта PageRequest
и передачи его методу репозитория:
@GetMapping("/users")
public Page<User> findAllUsers() {
Pageable pageable = PageRequest.of(0, 5);
return userRepository.findAll(pageable);
}
Первый параметр — это индекс страницы, отсчитываемый от нуля, а второй — размер страницы, которую мы хотим получить.
В приведенном выше примере мы создали объект PageRequest из сущностей
пользователя
, начиная с первой страницы ( 0
), где страница имеет 5
записей.
Кроме того, мы можем создать объект PageRequest
, используя параметры запроса страницы
и размера
:
@GetMapping("/users")
public Page<User> findAllUsers(@RequestParam("page") int page,
@RequestParam("size") int size, Pageable pageable) {
return userRepository.findAll(pageable);
}
Используя эту реализацию, запрос GET к конечной точке http://localhost:8080/users?page=0&size=2
вернет первую страницу объектов User
, а размер страницы результата будет равен 2:
{
"content": [
{
"id": 1,
"name": "John"
},
{
"id": 2,
"name": "Robert"
}
],
// continues with pageable metadata
}
6. Класс SortHandlerMethodArgumentResolver
Пейджинг фактически является подходом к эффективному управлению большим количеством записей в базе данных. Но сам по себе он довольно бесполезен, если мы не можем отсортировать записи каким-то определенным образом.
С этой целью Spring MVC предоставляет класс SortHandlerMethodArgumentResolver
. Преобразователь автоматически создает экземпляры Sort
из параметров запроса или из аннотаций @SortDefault
.
6.1. Использование параметра метода контроллера сортировки
Чтобы получить четкое представление о том, как работает класс SortHandlerMethodArgumentResolver
, добавим в класс контроллера метод findAllUsersSortedByName()
:
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@RequestParam("sort") String sort, Pageable pageable) {
return userRepository.findAll(pageable);
}
В этом случае класс SortHandlerMethodArgumentResolver
создаст объект Sort
, используя параметр запроса сортировки .
В результате запрос GET к конечной точке http://localhost:8080/sortedusers?sort=name
вернет массив JSON со списком объектов User
, отсортированных по свойству name :
{
"content": [
{
"id": 4,
"name": "Helen"
},
{
"id": 1,
"name": "John"
},
{
"id": 5,
"name": "Mary"
},
{
"id": 3,
"name": "Nataly"
},
{
"id": 2,
"name": "Robert"
}
],
// continues with pageable metadata
}
6.2. Использование статического фабричного метода Sort.by()
В качестве альтернативы мы можем создать объект Sort
с помощью статического фабричного метода Sort.by()
, который принимает ненулевой, непустой массив
свойств String
для сортировки.
В этом случае мы будем сортировать записи только по свойству имени :
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName() {
Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
return userRepository.findAll(pageable);
}
Конечно, мы могли бы использовать несколько свойств, если они объявлены в классе предметной области.
6.3. Использование аннотации @SortDefault
Точно так же мы можем использовать аннотацию @SortDefault
и получить те же результаты:
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@SortDefault(sort = "name",
direction = Sort.Direction.ASC) Pageable pageable) {
return userRepository.findAll(pageable);
}
Наконец, давайте создадим интеграционный тест, чтобы проверить поведение метода:
@Test
public void whenGetRequestToSorteredUsersEndPoint_thenCorrectResponse() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/sortedusers")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$['sort']['sorted']").value("true"));
}
7. Веб-поддержка Querydsl
Как мы упоминали во введении, веб-поддержка Spring Data позволяет нам использовать параметры запроса в методах контроллера для построения типов Predicate
Querydsl и построения запросов Querydsl. ``
Для простоты мы просто посмотрим, как Spring MVC преобразует параметр запроса в Querydsl BooleanExpression
, который, в свою очередь, передается в QuerydslPredicateExecutor
.
Для этого сначала нам нужно добавить зависимости Maven querydsl-apt
и querydsl-jpa
в файл pom.xml
:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
Далее нам нужно провести рефакторинг нашего интерфейса UserRepository
, который также должен расширять интерфейс QuerydslPredicateExecutor
:
@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long>,
QuerydslPredicateExecutor<User> {
}
Наконец, добавим в класс UserController следующий метод:
@GetMapping("/filteredusers")
public Iterable<User> getUsersByQuerydslPredicate(@QuerydslPredicate(root = User.class)
Predicate predicate) {
return userRepository.findAll(predicate);
}
Хотя реализация метода выглядит довольно простой, на самом деле она раскрывает множество функциональных возможностей.
Допустим, мы хотим получить из базы данных все сущности пользователя
, которые соответствуют заданному имени. Мы можем добиться этого , просто вызвав метод и указав параметр запроса имени
в URL-адресе :
http://localhost:8080/filteredusers?name=Джон
Как и ожидалось, запрос вернет следующий результат:
[
{
"id": 1,
"name": "John"
}
]
Как и раньше, мы можем использовать интеграционный тест для проверки метода getUsersByQuerydslPredicate()
:
@Test
public void whenGetRequestToFilteredUsersEndPoint_thenCorrectResponse() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/filteredusers")
.param("name", "John")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("John"));
}
Это всего лишь базовый пример того, как работает веб-поддержка Querydsl. Но на самом деле он не раскрывает всей своей силы. **
**
Теперь предположим, что мы хотим получить сущность пользователя
, которая соответствует заданному идентификатору.
В таком случае нам просто нужно передать параметр запроса id
в URL :
http://локальный:8080/filteredusers?id=2
В этом случае мы получим такой результат:
[
{
"id": 2,
"name": "Robert"
}
]
Понятно, что веб-поддержка Querydsl — очень мощная функция, которую мы можем использовать для извлечения записей из базы данных, соответствующих заданному условию.
Во всех случаях весь процесс сводится к простому вызову одного метода контроллера с разными параметрами запроса .
8. Заключение
В этом руководстве мы подробно рассмотрели ключевые компоненты веб-поддержки Spring и узнали, как использовать их в демонстрационном проекте Spring Boot.
Как обычно, все примеры, показанные в этом руководстве, доступны на GitHub .