Какой лучший метод 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.)

bzlm
источник
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
Lepidopteron
источник
3

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

GET
/api/members/count

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

willcodejavaforfood
источник
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
0

Вы могли бы рассмотреть countsкак ресурс. URL будет тогда:

/api/counts/member
Фрэнк Рем
источник
-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>
Wooff
источник