Який найкращий метод RESTful для повернення загальної кількості елементів в об’єкті?


139

Я розробляю службу API REST для великого веб-сайту в соціальних мережах, в якому я беру участь. Поки що це чудово працює. Я можу видати GET, POST, PUTі DELETEзапити на об'єкт URL - адрес і впливає на мої дані. Однак ці дані піддаються підключенню до сторінки (обмежено 30 результатами одночасно).

Однак який би був найкращий спосіб RESTful отримати загальну кількість скажіть членів за допомогою мого API?

Наразі я надсилаю запити до структури URL-адрес, таких як:

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

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


Відповіді:


84

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

API StackOverflow - хороший приклад того самого дизайну. Ось документація щодо способу "Користувачі" - https://api.stackexchange.com/docs/users


3
+1: Безумовно, найрізноманітніше, що потрібно зробити, якщо взагалі будуть накладені обмеження на отримання.
Дональні стипендіати

2
@bzim Ви знаєте, що має бути наступна сторінка для отримання, оскільки є посилання з rel = "next".
Даррел Міллер

4
@Donal "наступний" реєстр зареєстрований у IANA iana.org/assignments/link-relations/link-relations.txt
Даррел Міллер

1
@Darrel - так, це можна зробити з будь-яким типом "наступного" прапора в корисному навантаженні. Я просто відчуваю, що наявність загальної кількості предметів колекції у відповіді цінна сама по собі і працює як "наступний" прапор так само.
Франці Пенов

5
Повернення об'єкта, який не є списком елемента, не є належним реалізацією API REST, але REST не дає жодного способу отримати частковий список результатів. Отже, для цього, я думаю, що ми повинні використовувати заголовки для передачі іншої інформації, такої як загальна сума, наступний маркер сторінки та маркер попередньої сторінки. Я ніколи цього не пробував, і мені потрібні поради інших розробників.
Лонікс

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

Останнім часом я проводив обширне дослідження цього та інших питань, пов’язаних з ПЕСТ-сторінками, і вважаю, що це додати тут деякі мої висновки. Я трохи розширюю питання, щоб включити думки про пейджинговий зв’язок, а також про кількість підрахунків, оскільки вони тісно пов'язані.

Заголовки

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

Існує купа (стандартних та користувацьких) заголовків, які використовуються в дикій природі для повернення інформації, пов’язаної з підказками, включаючи загальну кількість.

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: items 0-49/234

Промотувана статтею блогу під назвою Заголовок діапазону, я вибираю вас (для пагінації)! . Автор робить вагомий аргумент для використання заголовків Rangeі Content-Rangeзаголовків для сторінки. Коли ми уважно прочитаємо в RFC на ці заголовки, ми знаходимо , що розширення їх значення за межі діапазонів байтів фактично порочить RFC і явно дозволені. При використанні в контексті itemsзамість цього bytes, заголовок Діапазону насправді дає нам можливість запитувати певний діапазон елементів та вказувати, до якого діапазону загального результату відносяться елементи відповіді. Цей заголовок також дає чудовий спосіб показати загальну кількість. І це справжній стандарт, який здебільшого відображає один на один на пейджингові. Також використовується в дикій природі .

Конверт

Багато API, в тому числі і наш улюблений веб-сайт Q&A, використовують конверт , обгортку навколо даних, яка використовується для додавання метаінформації про дані. Також стандарти 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 та використання його для того, на що він був призначений. В цьому випадку слід використовувати діапазон вмісту (RFC7233). Рішення всередині організму не є корисними, тим більше, що це не буде працювати з HEAD. створення нових заголовків, як тут пропонується, непотрібні та неправильні.
Vance Shipley

23

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

(Або, якщо ви перебуваєте в контрольованому середовищі від кінцевої точки до кінцевої точки, ви можете використовувати спеціальне дієслово HTTP, наприклад COUNT.)


4
"Спеціальний заголовок HTTP"? Це підпадає під заголовок дещо дивного, що, в свою чергу, суперечить тому, що, на мій погляд, повинен бути API RESTful. Зрештою, це має бути не дивно.
стипендіати доналу

21
@Donal я знаю. Але всі хороші відповіді вже були взяті. :(
bzlm

1
Я теж знаю, але іноді вам просто потрібно дозволити іншим відповідати. Або покращуйте свій внесок іншими способами, наприклад, детальним поясненням, чому це слід робити найкращим чином, а не іншими.
Стипендіати Доналу

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

1
Мені дуже подобається використовувати заголовки HTTP для подібних речей (саме там він належить). У цьому випадку може бути доречним стандартний заголовок посилання (API Github використовує це).
Майк Маркаччі

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 МОЖЕ Ігнорувати діапазони, вказані за допомогою інших одиниць.

Що вказує: використання спеціальних одиниць діапазону не проти протоколу, але його МОЖЕ бути проігноровано.

Таким чином, нам слід було б встановити діапазон прийняття на "членів" або будь-якого типу одиниці, яку ми очікували. Крім того, також встановіть діапазон вмісту до поточного діапазону. (див .: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 )

У будь-якому випадку я б дотримувався рекомендації RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ) надсилати номер 206 замість 200:

Якщо всі передумови істинні, сервер підтримує
поле заголовка діапазону для цільового ресурсу, а вказаний діапазон (и) є
дійсними та задоволеними (як визначено у розділі 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 / члени? offset = 10 & limit = 20
Michał Ziobro

1
Також майте на увазі, що для багатьох типів сторінок не потрібно рахувати (наприклад, нескінченна прокрутка) - Навіщо обчислювати це, коли клієнту це може не знадобитися
до

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 doesnt ЗАПИТУЙТЕ масив як результат запиту, вам просто потрібно налаштувати свій ресурс з властивістю параметра object: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 перед існуючою кінцевою точкою. Отже, коли зазначено, відповідь містить лише метадані.

кінцева точка? filter = значення

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

кінцева точка? filter = значення & countOnly = вірно

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.