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

REST Pagination весной

Pagination в весенней службе REST – структура URI и передовая практика, Страница как ресурс против страницы как представление.

Автор оригинала: Eugen Paraschiv.

1. Обзор

Этот учебник будет посвящен внедрение pagination в API REST с использованием Spring MVC и Spring Data.

Дальнейшее чтение:

Пагинация с весенним REST и таблицей AngularJS

JPA Pagination

Обнаружение API REST и HATEOAS

2. Страница как ресурс против страницы как представление

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

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

Следующим вопросом в дизайне pagination в контексте REST является где включить информационный :

  • на пути URI: /foo/page/1
  • запрос URI: /foo?page-1

Имея в виду, что страница не является ресурсным , кодирование информации о странице в URI больше не является вариантом.

Мы собираемся использовать стандартный способ решения этой проблемы, кодирование информации paging в запросе URI.

3. Контроллер

Теперь, для реализации – Контроллер весеннего MVC для pagination прост :

@GetMapping(params = { "page", "size" })
public List findPaginated(@RequestParam("page") int page, 
  @RequestParam("size") int size, UriComponentsBuilder uriBuilder,
  HttpServletResponse response) {
    Page resultPage = service.findPaginated(page, size);
    if (page > resultPage.getTotalPages()) {
        throw new MyResourceNotFoundException();
    }
    eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent(
      Foo.class, uriBuilder, response, page, resultPage.getTotalPages(), size));

    return resultPage.getContent();
}

В этом примере мы вводим два параметра запроса, в том размер и страница, в методе Контролера через @RequestParam.

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

Мы также инъекционных как Http ответ и UriComponentsBuilder , чтобы помочь с Discoverability – которые мы разъединения через пользовательские события. Если это не является целью API, можно просто удалить пользовательское событие.

Наконец – обратите внимание, что в центре внимания этой статьи только REST и веб-слой – пойти глубже в части доступа к данным pagination вы можете проверить эту статью о Pagination с весенними данными.

4. Открытие для REST Pagination

В рамках pagination, удовлетворяя HATEOAS ограничение REST означает, что клиент API может обнаружить в следующем и предыдущие страниц на основе текущей страницы навигации. С этой целью мы будем использовать Ссылка ЗАголовок HTTP, в сочетании с “ в следующем “, “ prev “, “ первый ” и “ последний ” типы ссылок .

В REST, Обнаруживаемость является перекрестной проблемой применимы не только к конкретным операциям, но и к видам операций. Например, каждый раз, когда создается Ресурс, URI этого ресурса должен быть обнаруживается клиентом. Поскольку это требование имеет отношение к созданию любого ресурса, мы будем обрабатывать его отдельно.

Мы разъединим эти проблемы с помощью событий, как мы обсуждали в предыдущая статья, посвященная доступности службы REST. В случае pagination, событие – PaginatedResultsRetrievedEvent – выстрелил в слой контроллера. Затем мы будем реализовывать обнаруживаемость с пользовательским слушателем для этого события.

Короче говоря, слушатель проверит, допускает ли навигация в следующем , предыдущие , первый и последний Страниц. Если это произойдет – это добавить соответствующие ИРИС к ответу в качестве заголовка «Ссылка» http .

Давайте шаг за шагом. UriComponentsBuilder переданный от контроллера содержит только базовый URL (хост, порт и контекстный путь). Таким образом, мы должны добавить оставшиеся разделы:

void addLinkHeaderOnPagedResourceRetrieval(
 UriComponentsBuilder uriBuilder, HttpServletResponse response,
 Class clazz, int page, int totalPages, int size ){

   String resourceName = clazz.getSimpleName().toString().toLowerCase();
   uriBuilder.path( "/admin/" + resourceName );

    // ...
   
}

Далее, мы будем использовать СтрингДжойнер для конкатенации каждого звена. Мы будем использовать uriBuilder для создания ИРИС. Давайте посмотрим, как мы будем действовать со ссылкой на в следующем страница:

StringJoiner linkHeader = new StringJoiner(", ");
if (hasNextPage(page, totalPages)){
    String uriForNextPage = constructNextPageUri(uriBuilder, page, size);
    linkHeader.add(createLinkHeader(uriForNextPage, "next"));
}

Давайте посмотрим на логику конструкцияNextPageUri метод:

String constructNextPageUri(UriComponentsBuilder uriBuilder, int page, int size) {
    return uriBuilder.replaceQueryParam(PAGE, page + 1)
      .replaceQueryParam("size", size)
      .build()
      .encode()
      .toUriString();
}

Мы будем действовать аналогичным образом для остальных URIs, которые мы хотим включить.

Наконец, мы добавим выход в качестве заголовка ответа:

response.addHeader("Link", linkHeader.toString());

Обратите внимание, что для краткости, я включил только частичный образец кода и полный код здесь .

5. Тест вождения Pagination

И основная логика pagination, и обнаруживаемость охватываются небольшими, сфокусированными интеграционными тестами. Как и в предыдущая статья , мы будем использовать БИБЛИОТЕКА, гарантированная REST для потребления службы REST и проверки результатов.

Вот несколько примеров пагинации интеграционных тестов; для полного набора тестов ознакомьтесь с проектом GitHub (ссылка в конце статьи):

@Test
public void whenResourcesAreRetrievedPaged_then200IsReceived(){
    Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2");

    assertThat(response.getStatusCode(), is(200));
}
@Test
public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived(){
    String url = getFooURL() + "?page=" + randomNumeric(5) + "&size=2";
    Response response = RestAssured.get.get(url);

    assertThat(response.getStatusCode(), is(404));
}
@Test
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources(){
   createResource();
   Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2");

   assertFalse(response.body().as(List.class).isEmpty());
}

6. Тест вождения Pagination Открытие

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

Тесты будут сосредоточены на положении текущей страницы в навигационном и различные УРИ, которые должны быть обнаружимы с каждой позиции:

@Test
public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext(){
   Response response = RestAssured.get(getFooURL()+"?page=0&size=2");

   String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next");
   assertEquals(getFooURL()+"?page=1&size=2", uriToNextPage);
}
@Test
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage(){
   Response response = RestAssured.get(getFooURL()+"?page=0&size=2");

   String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev");
   assertNull(uriToPrevPage );
}
@Test
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious(){
   Response response = RestAssured.get(getFooURL()+"?page=1&size=2");

   String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev");
   assertEquals(getFooURL()+"?page=0&size=2", uriToPrevPage);
}
@Test
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable(){
   Response first = RestAssured.get(getFooURL()+"?page=0&size=2");
   String uriToLastPage = extractURIByRel(first.getHeader("Link"), "last");

   Response response = RestAssured.get(uriToLastPage);

   String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next");
   assertNull(uriToNextPage);
}

Обратите внимание, что полный низкоуровневый код для extractURIByRel – ответственный за извлечение ИРИС rel отношения здесь .

7. Получение всех ресурсов

На ту же тему pagination и обнаруживаемости, выбор должен быть сделан, если клиенту разрешено получить все ресурсы в системе сразу, или если клиент должен попросить их paginated .

Если сделан выбор, что клиент не может получить все ресурсы с одного запроса, и pagination не является необязательным, но требуется, то несколько вариантов доступны для ответа на получить все запросы. Один из вариантов заключается в том, чтобы вернуть 404 ( Не найдено ) и использовать Ссылка заголовок, чтобы сделать первую страницу обнаружить:

Ссылка;, >;

Другой вариант – вернуть перенаправление – 303 (См. другие ) – на первую страницу. Более консервативным маршрутом было бы просто вернуть клиенту 405 ( Метод не допускается) для запроса GET.

8. REST Paging с диапазоном HTTP Хедерс

Относительно другой способ реализации pagination заключается в работе с HTTP Диапазон головкиДиапазон , Контент-диапазон , If-Range , Принять-Диапазоны – и Коды статуса HTTP – 206 ( Частичное содержание ), 413 ( Запрос сущность слишком большой ), 416 ( Запрошенный диапазон не удовлетворяется ).

Один из представлений об этом подходе заключается в том, что расширения диапазона HTTP не предназначены для pagination и что ими должен управлять Сервер, а не Приложение. Реализация pagination на основе расширений заголовка HTTP Range, тем не менее, технически возможна, хотя и не так часто, как реализация, обсуждаемая в этой статье.

9. Весенние данные REST Pagination

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

Весенние данные REST автоматически распознает параметры URL, такие как страница, размер, сортировка и так далее.

Для использования методов paging любого репозитория нам необходимо расширить PagingAndSortingRepository:

public interface SubjectRepository extends PagingAndSortingRepository{}

Если мы позвоним http://localhost:8080/subjects Весна автоматически добавляет страница, размер, сортировка параметры предложения с API:

"_links" : {
  "self" : {
    "href" : "http://localhost:8080/subjects{?page,size,sort}",
    "templated" : true
  }
}

По умолчанию размер страницы составляет 20, но мы можем изменить его, позвонив что-то вроде http://localhost:8080/subjects?page=10.

Если мы хотим внедрить paging в наш собственный пользовательский API репозитория, мы должны пройти дополнительный Страница параметр и убедитесь, что API возвращает страница:

@RestResource(path = "nameContains")
public Page findByNameContaining(@Param("name") String name, Pageable p);

Всякий раз, когда мы добавляем пользовательский API /поиск конечная точка добавляется к генерируемым ссылкам. Так что, если мы позвоним http://localhost:8080/subjects/search мы увидим, pagination способны конечной точки:

"findByNameContaining" : {
  "href" : "http://localhost:8080/subjects/search/nameContains{?name,page,size,sort}",
  "templated" : true
}

Все API, которые реализуют PagingAndSortingRepository вернет страница. Если нам нужно вернуть список результатов с Страница, getContent () API Страница предоставляет список записей, полученных в результате API Spring Data REST.

Код в этом разделе доступен в весна-данные-отдых проект.

10. Преобразование списка в Страницу

Допустим, у нас есть Страница объект в качестве ввода, но информация, которую нам нужно получить, содержится в списке, а не PagingAndSortingRepository . В этих случаях нам, возможно, придется конвертировать Список в Страница .

Например, представьте, что у нас есть список результатов от службы SOAP:

List list = getListOfFooFromSoapService();

Мы должны получить доступ к списку в конкретных позициях, определенных Страница объект послал к нам. Итак, давайте определим стартовый индекс:

int start = (int) pageable.getOffset();

И конечный индекс:

int end = (int) ((start + pageable.getPageSize()) > fooList.size() ? fooList.size()
  : (start + pageable.getPageSize()));

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

Page page 
  = new PageImpl(fooList.subList(start, end), pageable, fooList.size());

Ну вот! Мы можем вернуться сейчас страница как действительный результат.

И обратите внимание, что если мы также хотим, чтобы поддержать сортировка , мы должны сортировать список перед суб-листинг оно.

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

Эта статья проиллюстрировала, как реализовать Pagination в API REST с помощью Spring, и обсудила, как настроить и протестировать Discoverability.

Если вы хотите пойти в глубину на pagination в уровне настойчивости, проверить мои JPA или Hibernate pagination учебники.

Реализация всех этих примеров и фрагментов кода можно найти в Проект GitHub – это Maven основе проекта, поэтому она должна быть легко импортировать и работать, как она есть.