Какой лучший метод RESTful для возврата общего количества элементов в объекте?


139

Я разрабатываю сервис REST API для большого сайта социальной сети, в котором я участвую. Пока что он работает отлично. Я могу выдать GET, POST, PUTи DELETEзапросы на объект URL - адресов и влияет на мои данные. Однако эти данные разбиты на страницы (ограничено 30 результатами одновременно).

Тем не менее, что было бы лучшим RESTful-способом для получения общего числа участников, скажем, через мой API?

В настоящее время я выдаю запросы к структуре URL, например:

  • / api / members - возвращает список участников (30 одновременно, как указано выше)
  • / api / members / 1 - влияет на одного члена в зависимости от используемого метода запроса

Мой вопрос: как бы я тогда использовал похожую структуру URL, чтобы получить общее количество участников в моем приложении? Очевидно, что запрос только idполя (аналогично API Graph в Facebook) и подсчет результатов будут неэффективными, поскольку будет возвращен только фрагмент из 30 результатов.


Ответы:


84

Хотя ответ на / API / users разбит на страницы и возвращает только 30 записей, ничто не мешает вам включить в ответ также общее количество записей и другую соответствующую информацию, такую ​​как размер страницы, номер страницы / смещение и т. Д. ,

API StackOverflow - хороший пример того же дизайна. Вот документация для метода Users - https://api.stackexchange.com/docs/users


3
+1: Определенно самая ОТДЕЛЬНАЯ вещь, которую нужно сделать, если ограничения на выборку вообще будут навязываться.
— Donal Fellows

2
@bzim Вы бы знали, что нужно выбрать следующую страницу, потому что есть ссылка с rel = "next".
— Даррел Миллер

4
@ Донал «следующий» релиз
— Даррел Миллер,

1
@Darrel - да, это можно сделать с любым типом «следующего» флага в полезной нагрузке. Я просто чувствую, что общее количество элементов коллекции в ответе само по себе ценно и работает как «следующий» флаг.
— Франси Пенов

5
Возвращать объект, который не является списком элементов, не является правильной реализацией REST API, но REST не предоставляет никакого способа получить частичный список результатов. Поэтому, с учетом этого, я думаю, что мы должны использовать заголовки для передачи другой информации, такой как итог, токен следующей страницы и токен предыдущей страницы. Я никогда не пробовал, и мне нужен совет от других разработчиков.
— Loenix

74

Я предпочитаю использовать заголовки HTTP для такого рода контекстной информации.

Для общего количества элементов я использую X-total-countзаголовок.
Для ссылок на следующую, предыдущую страницу и т. Д. Я использую Linkзаголовок http :
http://www.w3.org/wiki/LinkHeader

Github делает это так же: https://developer.github.com/v3/#pagination

На мой взгляд, он чище, поскольку его можно использовать и при возврате контента, который не поддерживает гиперссылки (например, двоичные файлы, изображения).


5
RFC6648 осуждает соглашение о добавлении префикса имен нестандартных параметров в строку X-.
— JDawg

70

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

Заголовки

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

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

X-Total-Count

X-Total-Count: 234

Это используется в некоторых API, которые я нашел в дикой природе. Существуют также пакеты NPM для добавления поддержки этого заголовка, например, в Loopback. Некоторые статьи также рекомендуют устанавливать этот заголовок.

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

Ссылка на сайт

Link: </TheBook/chapter2>;
      rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>;
      rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

Из прочтения этой темы я чувствую, что общий консенсус заключается в том, чтобы использовать Linkзаголовок для предоставления пейджинговых ссылок клиентам, использующим rel=nextи rel=previousт. Д. Проблема в том, что ему не хватает информации о том, сколько всего записей, что почему многие API объединяют это с X-Total-Countзаголовком.

В качестве альтернативы некоторые API и, например, стандарт JsonApi , используют Linkформат, но добавляют информацию в конверт ответа, а не в заголовок. Это упрощает доступ к метаданным (и создает место для добавления информации об общем количестве) за счет увеличения сложности доступа к самим фактическим данным (путем добавления конверта).

Content-Range

Content-Range: items 0-49/234

При поддержке статьи в блоге под названием Range header я выбираю вас (для нумерации страниц)! , Автор приводит веские аргументы для используя Rangeи Content-Rangeзаголовков для пагинации. Когда мы внимательно прочитаем в RFC на эти заголовки, мы находим , что расширение их значения за пределы диапазонов байтов фактически порочит RFC и явно разрешены. При использовании itemsвместо контекста bytesзаголовок Range фактически дает нам возможность как запросить определенный диапазон элементов, так и указать, к какому диапазону общего результата относятся элементы ответа. Этот заголовок также дает отличный способ показать общее количество. И это настоящий стандарт, который в основном отображает один на один для пейджинга. Это также используется в дикой природе .

Конверт

Многие API, в том числе и на нашем любимом сайте вопросов и ответов, используют конверт , обертку вокруг данных, которая используется для добавления метаинформации о данных. Кроме того, стандарты OData и JsonApi используют конверт ответа.

Большим недостатком этого (imho) является то, что обработка данных ответа становится более сложной, так как фактические данные должны быть найдены где-то в конверте. Также есть много разных форматов для этого конверта, и вы должны использовать правильный. Это говорит о том, что конверты ответа от OData и JsonApi сильно отличаются друг от друга, причем OData смешивает метаданные в нескольких точках ответа.

Отдельная конечная точка

Я думаю, что это было достаточно освещено в других ответах. Я не исследовал это много, потому что я согласен с комментариями, что это сбивает с толку, поскольку у вас теперь есть несколько типов конечных точек. Я думаю, что было бы лучше, если бы каждая конечная точка представляла (коллекцию) ресурс (ы).

Дальнейшие мысли

Нам нужно не только передавать метаинформацию подкачки, связанную с ответом, но и разрешать клиенту запрашивать определенные страницы / диапазоны. Интересно также взглянуть на этот аспект, чтобы получить согласованное решение. Здесь также мы можем использовать заголовки ( Rangeзаголовок кажется очень подходящим) или другие механизмы, такие как параметры запроса. Некоторые люди выступают за обработку страниц результатов , как отдельные ресурсы, которые могут иметь смысл в некоторых случаях использования (например /books/231/pages/52, я в конечном итоге выбор дикого диапазона часто используемых параметры запроса , такие как pagesize, page[size]и limitт.д. , в дополнении к поддержке Rangeзаголовка (и в качестве параметра запроса также).


Меня особенно интересовал Rangeзаголовок, однако я не смог найти достаточно доказательств того, что использование чего-либо, кроме bytesтипа диапазона, является допустимым.
— VisioN

2
Я думаю, что самое ясное доказательство можно найти в разделе 14.5 RFC : acceptable-ranges = 1#range-unit | "none"я думаю, что эта формулировка явно оставляет место для других единиц измерения дальности bytes, хотя сама спецификация только определяет bytes.
— Стейн де Витт

24

Альтернатива, когда вам не нужны реальные предметы

Ответ Франци Пенова, безусловно, лучший путь, поэтому вы всегда возвращаете элементы вместе со всеми дополнительными метаданными о запрашиваемых объектах. Так и должно быть.

но иногда возвращать все данные не имеет смысла, потому что они могут вам вообще не понадобиться. Может быть, все, что вам нужно, это метаданные о вашем запрашиваемом ресурсе. Как общее количество или количество страниц или что-то еще. В таком случае вы всегда можете сделать так, чтобы URL-запрос указывал вашей службе не возвращать элементы, а просто метаданные, например:

/api/members?metaonly=true
/api/members?includeitems=0

или что-то подобное ...


10
Встраивание этой информации в заголовки имеет то преимущество, что вы можете сделать запрос HEAD, чтобы просто получить счетчик.
— felixfbecker

1
Точно, @felixfbecker, спасибо за то, что заново изобрели колесо и загромождали API всеми видами механизмов :)
— EralpB

1
@EralpB Спасибо за изобретение колеса и загромождение API !? HEAD выделяется в HTTP. metaonlyили includeitemsнет.
— felixfbecker

2
@felixfbecker только «точно» предназначался для вас, остальное для ОП. Извините за путаницу.
— EralpB

REST - это все, что позволяет использовать HTTP и максимально использовать его для того, для чего он предназначен. В этом случае следует использовать Content-Range (RFC7233). Растворы в организме бесполезны, тем более что он не будет работать с HEAD. создание новых заголовков, как предлагается здесь, не нужно и неправильно.
— Вэнс Шипли

23

Вы можете вернуть счетчик как пользовательский заголовок HTTP в ответ на запрос HEAD. Таким образом, если клиенту нужен только счет, вам не нужно возвращать фактический список, и нет необходимости в дополнительном URL.

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


4
«Пользовательский заголовок HTTP»? Это было бы несколько удивительно, что, в свою очередь, противоречит тому, что я считаю RESTful API. В конечном счете, это должно быть неудивительно.
— Donal Fellows

21
@ Донал, я знаю. Но все хорошие ответы уже были приняты. :(
— bzlm

1
Я тоже знаю, но иногда нужно просто позволить другим людям ответить. Или сделайте свой вклад лучше другими способами, такими как подробное объяснение того, почему это должно быть сделано лучше, чем другие.
— Donal Fellows

4
В контролируемой среде это вполне может быть неудивительным, поскольку, скорее всего, оно будет использоваться внутри и основано на API-политике ваших разработчиков. Я бы сказал, что в некоторых случаях это было хорошее решение, и его стоит упомянуть в качестве возможного необычного решения.
— Джеймс Биллингем

1
Мне очень нравится использовать заголовки HTTP для такого рода вещей (это действительно то, где он принадлежит). Стандартный заголовок Link может быть уместным в этом случае (Github API использует это).
— Майк Маркаччи

11

Я бы порекомендовал добавить заголовки для того же, например:

HTTP/1.1 200

Pagination-Count: 100
Pagination-Page: 5
Pagination-Limit: 20
Content-Type: application/json

[
  {
    "id": 10,
    "name": "shirt",
    "color": "red",
    "price": "$23"
  },
  {
    "id": 11,
    "name": "shirt",
    "color": "blue",
    "price": "$25"
  }
]

За подробностями обращайтесь к:

https://github.com/adnan-kamili/rest-api-response-format

Для файла чванства:

https://github.com/adnan-kamili/swagger-response-template


7

Начиная с "X -" - Префикс устарел. (см .: https://tools.ietf.org/html/rfc6648 )

Мы нашли «Accept-Ranges» как лучшую ставку для отображения разбивки на страницы: https://tools.ietf.org/html/rfc7233#section-2.3 Поскольку «Единицы измерения» могут быть либо «байтами», либо « лексема». Оба не представляют пользовательский тип данных. (см .: https://tools.ietf.org/html/rfc7233#section-4.2 ) Тем не менее, заявлено, что

Реализации HTTP / 1.1 МОГУТ игнорировать диапазоны, указанные с использованием других модулей.

Что указывает: использование пользовательских единиц измерения диапазона не противоречит протоколу, но МОЖЕТ игнорироваться.

Таким образом, мы должны были бы установить Accept-Ranges на «members» или любой другой тип дальнобойного подразделения, как мы и ожидали. И, кроме того, также установите Content-Range на текущий диапазон. (см .: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 )

В любом случае, я бы придерживался рекомендации RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ) для отправки 206 вместо 200:

Если все предварительные условия выполняются, сервер поддерживает
поле заголовка Range для целевого ресурса, и указанные диапазоны являются
действительными и выполнимыми (как определено в разделе 2.1), серверу СЛЕДУЕТ
отправить ответ 206 (частичное содержимое) с полезной нагрузкой, содержащей одно
или несколько частичных представлений, которые соответствуют
запрошенным допустимым диапазонам, как определено в разделе 4.

Таким образом, в результате мы получили бы следующие поля заголовка HTTP:

Для Частичного Содержания:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

Для полного содержания:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20

3

Кажется, проще всего просто добавить

GET
/api/members/count

и вернуть общее количество членов


11
Не хорошая идея. Вы обязываете клиентов сделать 2 запроса на построение нумерации страниц на своих страницах. Первый запрос для получения списка ресурсов, а второй для подсчета итогов.
— Джекис

Я думаю, что это хороший подход ... вы также можете вернуть только список результатов в виде json, а на стороне клиента проверить размер коллекции, так что такой случай - глупый пример ... более того, вы можете иметь / api / members / count и затем / api / members? offset = 10 & limit = 20
— Михал Зиобро

1
Также имейте в виду, что многие типы нумерации страниц не требуют подсчета (например, бесконечная прокрутка). Зачем рассчитывать это, когда клиенту это может не понадобиться
— tofarr

2

Как насчет новой конечной точки> / api / members / count, которая просто вызывает Members.Count () и возвращает результат


27
Предоставление счетчику явной конечной точки делает его автономным адресуемым ресурсом. Это сработает, но поднимет интересные вопросы для всех, кто не знаком с вашим API. Является ли количество членов коллекции отдельным ресурсом из коллекции? Могу ли я обновить его с помощью запроса PUT? Существует ли она для пустой коллекции или только если в ней есть предметы? Если membersколлекция может быть создана с помощью запроса POST /api, будет /api/members/countли она также создана как побочный эффект, или я должен выполнить явный запрос POST, чтобы создать ее, прежде чем запрашивать ее? :-)
— Франци Пенов

2

Иногда фреймворкам (таким как $ resource / AngularJS) требуется массив в качестве результата запроса, и вы не можете получить ответ, как {count:10,items:[...]}в этом случае я храню «count» в responseHeaders.

PS На самом деле вы можете сделать это с помощью $ resource / AngularJS, но для этого нужны некоторые настройки.


Что это за настройки? Они будут полезны для вопросов, подобных этому: stackoverflow.com/questions/19140017/…
— JBCP

Angular НЕ ТРЕБУЕТ массив как результат запроса, вам просто нужно настроить свой ресурс с помощью свойства объекта option:isArray: false|true
— Rémi Becheras


-1

При запросе разбитых на страницы данных вы знаете (по явному значению параметра размера страницы или значению размера страницы по умолчанию) размер страницы, чтобы вы знали, получили ли вы все данные в ответ или нет. Когда в ответе меньше данных, чем размер страницы, вы получаете целые данные. Когда полная страница возвращается, вы должны снова попросить другую страницу.

Я предпочитаю иметь отдельную конечную точку для подсчета (или такую ​​же конечную точку с параметром countOnly). Потому что вы могли бы подготовить конечного пользователя к длительному / длительному процессу, показав правильно инициированный индикатор выполнения.

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

<data>
  <originalRequest>
    <filter/>
    <filter/>
  </originalReqeust>
  <totalRecordCount/>
  <pageSize/>
  <offset/>
  <list>
     <item/>
     <item/>
  </list>
</data>

Моя связь, предпочитаю параметр countOnly существующей конечной точке. Таким образом, когда указано, ответ содержит только метаданные.

конечная точка? фильтр = значение

<data>
  <count/>
  <list>
    <item/>
    ...
  </list>
</data>

конечная точка? фильтр = значение & countOnly = TRUE

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.