Перейти к основному содержимому

Веб-поддержка данных Spring

· 11 мин. чтения

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 .