REST Pagination весной

REST Pagination весной

1. обзор

В этом руководстве основное внимание будет уделеноthe implementation of pagination in a REST API, using Spring MVC and Spring Data.

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

Нумерация страниц с помощью Spring REST и AngularJS таблицы

Подробно рассмотрим, как реализовать простой API с разбиением на страницы в Spring и как использовать его с AngularJS и UI Grid.

Read more

JPA Pagination

Разбивка на страницы в JPA - как использовать JQL и Criteria API для правильной разбивки на страницы.

Read more

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

HATEOAS и обнаружение службы REST - на основе тестов.

Read more

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

Первый вопрос при проектировании разбивки на страницы в контексте архитектуры RESTful заключается в том, следует ли учитыватьpage an actual Resource or just a Representation of Resources.

Если рассматривать саму страницу как ресурс, возникает множество проблем, таких как невозможность уникальной идентификации ресурсов между вызовами. Это, в сочетании с тем фактом, что на уровне сохраняемости страница не является надлежащим объектом, а является держателем, который создается при необходимости, делает выбор простым:the page is part of the representation.

Следующий вопрос в дизайне разбивки на страницы в контексте REST -where to include the paging information:

  • в пути URI:/foo/page/1

  • запрос URI:/foo?page=1

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

Мы собираемся использовать стандартный способ решения этой проблемыencoding the paging information in a URI query.

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

Теперь для реализации -the Spring MVC Controller for pagination is straightforward:

@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();
}

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

Alternatively, we could have used a Pageable object, which maps the pagesize, and sort parameters automatically. Кроме того, объектPagingAndSortingRepository предоставляет готовые методы, которые также поддерживают использованиеPageable в качестве параметра.

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

Наконец - обратите внимание, что в центре внимания этой статьи только REST и веб-уровень - чтобы углубиться в часть доступа к данным при разбивке на страницы, вы можетеcheck out this article о разбиении на страницы с помощью Spring Data.

4. Доступность для пагинации REST

В рамках разбиения на страницы выполнениеHATEOAS constraint of REST означает разрешение клиенту API обнаруживать страницыnext иprevious на основе текущей страницы в навигации. Для этогоwe’re going to use the Link HTTP header, coupled with the “next“, “prev“, “first” and “last” link relation types.

В RESTDiscoverability is a cross-cutting concern применимо не только к определенным операциям, но и к типам операций. Например, каждый раз, когда создается ресурс, URI этого ресурса должен быть обнаружен клиентом. Поскольку это требование актуально для создания ЛЮБОГО ресурса, мы обработаем его отдельно.

Мы отделим эти проблемы с помощью событий, как мы обсуждали вprevious article focusing on Discoverability службы REST. В случае разбивки на страницы событиеPaginatedResultsRetrievedEvent запускается на уровне контроллера. Затем мы реализуем возможность обнаружения с помощью специального прослушивателя для этого события.

Короче говоря, слушатель проверит, допускает ли навигация страницыnext,previous,first иlast. Если да - будетadd the relevant URIs to the response as a ‘Link' HTTP Header.

Пойдем шаг за шагом. 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 );

    // ...

}

Затем мы будем использоватьStringJoiner для объединения каждой ссылки. Мы будем использоватьuriBuilder для генерации URI. Посмотрим, как мы поступим со ссылкой на страницуnext:

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

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

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

Аналогичным образом поступим и с остальными URI, которые мы хотим включить.

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

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

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

5. Пробная разбивка на страницы

И основная логика разбиения на страницы, и обнаруживаемость охватываются небольшими целенаправленными интеграционными тестами. Как и в случае сprevious article, мы будем использоватьREST-assured library для использования службы 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. Проверка возможности обнаружения разбиения на страницы

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

The tests will focus on the position of the current page in navigation и различные URI, которые должны быть обнаружены из каждой позиции:

@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 - отвечает за извлечение URI по отношениюrelis here.

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

По той же теме разбивки на страницы и возможности обнаружения,the choice must be made if a client is allowed to retrieve all the Resources in the system at once, or if the client must ask for them paginated.

Если сделан выбор, что клиент не может извлечь все Ресурсы одним запросом, и разбиение на страницы не является обязательным, но требуется, тогда для ответа на запрос get all доступно несколько вариантов. Один из вариантов - вернуть 404 (Not Found) и использовать заголовокLink, чтобы первая страница стала доступной для обнаружения:

Другой вариант - вернуть редирект - 303(See Other) - на первую страницу. Более консервативный способ - просто вернуть клиенту 405 (Method Not Allowed) для запроса GET.

8. Пейджинг REST с заголовками HTTPRange

Относительно другой способ реализации разбивки на страницы - это работа сHTTP Range headers -Range,Content-Range,If-Range,Accept-Ranges - иHTTP status codes - 206 (Partial Content), 413 (Request Entity Too Large), 416 (Requested Range Not Satisfiable).

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

9. Spring Data REST Pagination

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

Spring Data REST автоматически распознает параметры URL, такие какpage, size, sort и т. д.

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

public interface SubjectRepository extends PagingAndSortingRepository{}

Если мы вызываемhttp://localhost:8080/subjects Spring, автоматически добавляются варианты параметровpage, size, sort с API:

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

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

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

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

Каждый раз, когда мы добавляем пользовательский API, конечная точка/search добавляется к созданным ссылкам. Итак, если мы вызовемhttp://localhost:8080/subjects/search we, мы увидим конечную точку с возможностью разбиения на страницы:

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

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

Код в этом разделе доступен в проектеspring-data-rest.

10. ПреобразованиеList вPage

Предположим, что у нас есть объектPageable в качестве входных данных, но информация, которую нам нужно получить, содержится в списке вместоPagingAndSortingRepository. В этих случаях нам может потребоватьсяconvert a List into a Page.

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

List list = getListOfFooFromSoapService();

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

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

И конец индекса:

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

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

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

Это оно! Теперь мы можем вернутьpage как действительный результат.

И обратите внимание, что если мы также хотим поддержатьsorting, нам нужно егоsort the list before sub-listing.

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

В этой статье показано, как реализовать разбиение на страницы в REST API с использованием Spring, а также рассказано, как настроить и проверить обнаруживаемость.

Если вы хотите углубиться в разбиение на страницы на уровне сохраняемости, ознакомьтесь с моими руководствами по разбивке на страницыJPA илиHibernate.

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