Рубрики
Без рубрики

Веб-поддержка Spring Data

Узнайте, как SpringDataWebSupport использует распознаватели для сокращения шаблонного кода и повышения выразительности контроллеров REST.

Автор оригинала: Alejandro Ugarte.

1. Обзор

Spring MVC и Spring Data каждый по-своему отлично справляются с задачей упрощения разработки приложений. Но что, если мы сложим их вместе?

В этом уроке мы рассмотрим веб-поддержку Spring Data и как ее распознаватели могут уменьшить шаблонность и сделать наши контроллеры более выразительными.

По пути мы рассмотрим Querydsl и то, как выглядит его интеграция с данными Spring.

2. Немного предыстории

Веб-поддержка Spring Data представляет собой набор веб-функций, реализованных поверх стандартной платформы Spring MVC, направленных на добавление дополнительных функций на уровень контроллера|/.

Функциональность веб-поддержки Spring Data основана на нескольких классах resolver . Распознаватели упрощают реализацию методов контроллера, которые взаимодействуют с хранилищами данных Spring , а также обогащают их дополнительными функциями.

Эти функции включают извлечение объектов домена из уровня репозитория, без необходимости явного вызова реализаций репозитория и построение ответов контроллера , которые могут быть отправлены клиентам в виде сегментов данных, поддерживающих разбиение на страницы и сортировку.

Кроме того, запросы к методам контроллера, которые принимают один или несколько параметров запроса, могут быть внутренне разрешены для запросов Querydsl.

3. Демонстрационный Проект Весенней Загрузки

Чтобы понять, как мы можем использовать SpringDatawebsupport для улучшения функциональности наших контроллеров, давайте создадим базовый проект Spring Boot.

Зависимости Maven нашего демонстрационного проекта довольно стандартны, за некоторыми исключениями, которые мы обсудим позже:


    org.springframework.boot
    spring-boot-starter-data-jpa


    org.springframework.boot
    spring-boot-starter-web


    com.h2database
    h2
    runtime


    org.springframework.boot
    spring-boot-starter-test
    test

В этом случае мы включили spring-boot-starter-web , поскольку мы будем использовать его для создания контроллера RESTful, spring-boot-starter-jpa для реализации уровня сохраняемости и spring-boot-starter-test для тестирования API контроллера.

Поскольку мы будем использовать H2 в качестве базовой базы данных, мы также включили com.h2database .

Давайте иметь в виду, что spring-boot-starter-web по умолчанию включает поддержку springdatawebs. Следовательно, нам не нужно создавать какие-либо дополнительные @Конфигурации классы, чтобы заставить его работать в нашем приложении.

И наоборот, для проектов, не связанных с загрузкой Spring, нам нужно будет определить класс @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 {}

В определении интерфейса UserRepository нет ничего изначально сложного, за исключением того, что он расширяет PagingAndSortingRepository .

Это сигнализирует Spring MVC о включении функций автоматической подкачки и сортировки записей базы данных .

3.3. Уровень Контроллера

Теперь нам нужно реализовать, по крайней мере, базовый контроллер RESTful, который действует как средний уровень между клиентом и уровнем репозитория.

Поэтому давайте создадим класс контроллера, который берет экземпляр UserRepository в своем конструкторе и добавляет один метод для поиска пользователей сущностей по идентификатору :

@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 path вместе с разрешимым экземпляром класса домена, мы автоматически запустили поиск объекта домена .

5. Класс PageableHandlerMethodArgumentResolver

Spring MVC поддерживает использование типов с возможностью просмотра страниц в контроллерах и репозиториях.

Проще говоря, экземпляр с возможностью просмотра по страницам – это объект, содержащий информацию о подкачке. Поэтому, когда мы передаем аргумент Pageable методу контроллера, Spring MVC использует PageableHandlerMethodArgumentResolver класс для преобразования экземпляра Pageable в объект PageRequest|/, который является простой реализацией Pageable .

5.1. Использование Pageable в качестве параметра метода контроллера

Чтобы понять, как работает класс PageableHandlerMethodArgumentResolver , давайте добавим новый метод в класс UserController :

@GetMapping("/users")
public Page findAllUsers(Pageable pageable) {
    return userRepository.findAll(pageable);
}

В отличие от метода findUserById () , здесь нам нужно вызвать реализацию репозитория, чтобы получить все объекты User JPA, сохраненные в базе данных.

Поскольку метод использует экземпляр с возможностью просмотра на странице , он возвращает подмножество всего набора сущностей, хранящихся в объекте Страница<Пользователь> .

Страница объект – это подсписок списка объектов , который предоставляет несколько методов, которые мы можем использовать для получения информации о результатах выгрузки , включая общее количество страниц результатов и номер страницы, которую мы извлекаем.

По умолчанию Spring MVC использует класс PageableHandlerMethodArgumentResolver для создания объекта Запроса страницы со следующими параметрами запроса:

  • страница : индекс страницы, которую мы хотим получить – параметр проиндексирован на ноль, и его значение по умолчанию равно 0
  • размер : количество страниц, которые мы хотим получить – значение по умолчанию 20
  • сортировка : одно или несколько свойств, которые мы можем использовать для сортировки результатов,используя следующий формат: свойство 1,свойство 2 (, asc|desc) – например, ? сортировка=имя и сортировка=электронная почта, asc

Например, запрос GET на http://localhost:8080/user s конечная точка вернет следующий вывод:

{
  "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. Это действительно полезно, так как интерфейс может использовать эти элементы для простого создания механизма подкачки.

Кроме того, мы можем использовать интеграционный тест для проверки метода найти всех пользователей() :

@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 findAllUsers(@PageableDefault(value = 2, page = 0) Pageable pageable) {
    return userRepository.findAll(pageable);
}

В качестве альтернативы мы можем использовать Запрос страницы s из() статического заводского метода для создания пользовательского Запроса страницы объекта и передачи его в метод репозитория:

@GetMapping("/users")
public Page findAllUsers() {
    Pageable pageable = PageRequest.of(0, 5);
    return userRepository.findAll(pageable);
}

Первый параметр-это индекс страницы на основе нуля, а второй-размер страницы, которую мы хотим получить.

В приведенном выше примере мы создали PageRequest объект Пользователя сущностей, начиная с первой страницы ( 0 ), при этом страница имеет 5 записи.

Кроме того, мы можем создать Запрос страницы объект, используя параметры страница и размер запрос:

@GetMapping("/users")
public Page 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 . Распознаватель автоматически создает Сортирует экземпляры из параметров запроса или из @SortDefault аннотации .

6.1. Использование параметра метода контроллера сортировки

Чтобы получить четкое представление о том, как работает класс SortHandlerMethodArgumentResolver , давайте добавим метод findAllUsersSortedByName() в класс контроллера:

@GetMapping("/sortedusers")
public Page findAllUsersSortedByName(@RequestParam("sort") String sort, Pageable pageable) {
    return userRepository.findAll(pageable);
}

В этом случае класс SortHandlerMethodArgumentResolver создаст объект Sort с помощью параметра sort request.

В результате запрос GET на http://localhost:8080/sortedusers?sort=name конечная точка вернет массив JSON со списком Пользователей объектов, отсортированных по имени свойству:

{
  "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.by() статический метод фабрики, который принимает ненулевой, непустой массив из Строки свойств для сортировки.

В этом случае мы будем сортировать записи только по имени свойству:

@GetMapping("/sortedusers")
public Page findAllUsersSortedByName() {
    Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
    return userRepository.findAll(pageable);
}

Конечно, мы могли бы использовать несколько свойств, если они объявлены в классе домена.

6.3. Использование аннотации @SortDefault

Аналогично, мы можем использовать аннотацию @SortDefault и получить те же результаты:

@GetMapping("/sortedusers")
public Page 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

Как мы упоминали во введении, SpringDatawebsupport позволяет нам использовать параметры запроса в методах контроллера для построения типов Querydsl ‘s Предикатов и для построения запросов Querydsl.

Чтобы все было просто, мы просто посмотрим , как Spring MVC преобразует параметр запроса в логическое выражение Querydsl , которое, в свою очередь, передается QueryDslPredicateExecutor .

Для этого сначала нам нужно добавить зависимости querydsl-apt и querydsl-jpa Maven в pom.xml файл:


    com.querydsl
    querydsl-apt


    com.querydsl
    querydsl-jpa

Затем нам нужно провести рефакторинг нашего интерфейса UserRepository , который также должен расширить интерфейс QueryDslPredicateExecutor :

@Repository
public interface UserRepository extends PagingAndSortingRepository,
  QuerydslPredicateExecutor {
}

Наконец, давайте добавим следующий метод в класс UserController :

@GetMapping("/filteredusers")
public Iterable getUsersByQuerydslPredicate(@QuerydslPredicate(root = User.class) 
  Predicate predicate) {
    return userRepository.findAll(predicate);
}

Хотя реализация метода выглядит довольно простой, на самом деле она раскрывает множество функций под поверхностью.

Допустим, мы хотим извлечь из базы данных все Пользовательские сущности, соответствующие заданному имени. Мы можем достичь этого , просто вызвав метод и указав имя параметр запроса в URL :

http://localhost:8080/filteredusers?name=John

Как и ожидалось, запрос вернет следующий результат:

[
  {
    "id": 1,
    "name": "John"
  }
]

Как и ранее, мы можем использовать интеграционный тест для проверки метода получить пользователей с помощью предиката Querydsl() :

@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. Но на самом деле это не раскрывает всей своей силы.

Теперь предположим, что мы хотим получить Пользователя сущность, соответствующую заданному идентификатору. В таком случае нам просто нужно передать идентификатор параметр запроса в URL :

http://localhost:8080/filteredusers?id=2

В этом случае мы получим такой результат:

[
  {
    "id": 2,
    "name": "Robert"
  }
]

Очевидно, что веб-поддержка Querydsl-это очень мощная функция, которую мы можем использовать для извлечения записей базы данных, соответствующих заданному условию.

Во всех случаях весь процесс сводится к простому вызову одного метода контроллера с различными параметрами запроса .

8. Заключение

В этом уроке мы подробно рассмотрели ключевые компоненты Spring web support и узнали, как их использовать в демонстрационном проекте Spring Boot.

Как обычно, все примеры, показанные в этом руководстве, доступны на GitHub .