Корисне навантаження відповіді на пагінацію з RESTful API


83

Я хочу підтримати пагінацію в своєму RESTful API.

Мій метод API повинен повертати JSON-список продуктів через /products/index. Однак потенційно існує тисячі товарів, і я хочу переглядати їх, тому мій запит повинен виглядати приблизно так:

/products/index?page_number=5&page_size=20

Але як має виглядати моя відповідь JSON? Чи сподіваються споживачі API у відповіді метаданих про пагінацію? Або необхідний лише масив продуктів? Чому?

Схоже, API Twitter включає метадані: https://dev.twitter.com/docs/api/1/get/lists/members (див. Приклад запиту).

З метаданими:

{
  "page_number": 5,
  "page_size": 20,
  "total_record_count": 521,
  "records": [
    {
      "id": 1,
      "name": "Widget #1"
    },
    {
      "id": 2,
      "name": "Widget #2"
    },
    {
      "id": 3,
      "name": "Widget #3"
    }
  ]
}

Просто масив продуктів (без метаданих):

[
  {
    "id": 1,
    "name": "Widget #1"
  },
  {
    "id": 2,
    "name": "Widget #2"
  },
  {
    "id": 3,
    "name": "Widget #3"
  }
]

Відповіді:


110

API ReSTful споживаються переважно іншими системами, саме тому я розміщую дані підкачки у заголовках відповідей. Однак деякі споживачі API можуть не мати прямого доступу до заголовків відповідей або можуть створювати UX над вашим API, тому надання способу отримання (на вимогу) метаданих у відповіді JSON є плюсом.

Я вважаю, що ваша реалізація повинна включати машиночитані метадані за замовчуванням та читані людиною метадані за запитом. Зчитані людиною метадані можуть бути повернуті з кожним запитом, якщо вам подобається, або, бажано, на вимогу, за допомогою параметра запиту, такого як include=metadataабо include_metadata=true.

У вашому конкретному сценарії я б включив URI для кожного продукту із записом. Це полегшує споживачеві API створення посилань на окремі продукти. Я б також встановив кілька розумних очікувань відповідно до меж моїх запитів на пейджинг. Впровадження та документування параметрів за замовчуванням для розміру сторінки є прийнятною практикою. Наприклад, API GitHub встановлює розмір сторінки за замовчуванням до 30 записів з максимальною кількістю 100, а також встановлює обмеження швидкості запитів до API. Якщо ваш API має розмір сторінки за замовчуванням, тоді рядок запиту може просто вказати індекс сторінки.

У зручному для читання сценарії під час переходу до /products?page=5&per_page=20&include=metadata, відповідь може бути:

{
  "_metadata": 
  {
      "page": 5,
      "per_page": 20,
      "page_count": 20,
      "total_count": 521,
      "Links": [
        {"self": "/products?page=5&per_page=20"},
        {"first": "/products?page=0&per_page=20"},
        {"previous": "/products?page=4&per_page=20"},
        {"next": "/products?page=6&per_page=20"},
        {"last": "/products?page=26&per_page=20"},
      ]
  },
  "records": [
    {
      "id": 1,
      "name": "Widget #1",
      "uri": "/products/1"
    },
    {
      "id": 2,
      "name": "Widget #2",
      "uri": "/products/2"
    },
    {
      "id": 3,
      "name": "Widget #3",
      "uri": "/products/3"
    }
  ]
}

Для машиночитаних метаданих я б додав заголовки Link до відповіді:

Link: </products?page=5&perPage=20>;rel=self,</products?page=0&perPage=20>;rel=first,</products?page=4&perPage=20>;rel=previous,</products?page=6&perPage=20>;rel=next,</products?page=26&perPage=20>;rel=last

(значення заголовка посилання має бути закодованим)

... і, можливо, спеціальний total-countзаголовок відповіді, якщо ви так вирішите:

total-count: 521

Інші дані підкачки, виявлені в метаданих, орієнтованих на людину, можуть бути зайвими для метаданих, орієнтованих на машину, оскільки заголовки посилань дають мені знати, на якій сторінці я перебуваю, і кількість на сторінці, і я можу швидко отримати кількість записів у масиві . Тому я, мабуть, створив би лише заголовок для загальної кількості. Ви завжди можете передумати згодом і додати більше метаданих.

Окрім того, ви можете помітити, що я видалив /indexваш URI. Загальновизнаною умовою є надання вашої кінцевої точки ReST колекціям. Маючи /indexв кінці каламутні, що трохи вгору.

Це лише кілька речей, які я люблю мати під час споживання / створення API. Сподіваюся, це допоможе!


per_page не відповідає умовам page_size
Alexandros Spyropoulos

1
"page_count": 20і {"last": "/products?page=26&per_page=20"}?
Джером

1
що станеться, якщо кількість продуктів раптово збільшиться під час отримання всіх записів зі сторінки 1 на сторінку x?
МеВ

3
@MeV те саме, що відбувається в будь-якому сценарії пагінації на основі курсору: загальна кількість збільшиться, а кількість сторінок може зрости, якщо остання сторінка заповнена, ні більше, ні менше. Це дуже поширений сценарій для кожного додатка, який використовує цей тип пагінації. Це залежить від використовуваного сортування, якщо новий продукт з’явиться на першій чи останній сторінці.
Пабло Пазос,

2
"ReSTful API споживають переважно інші системи, саме тому я розміщую дані підкачки у заголовках відповідей" Це все одно, що сказати, що надворі сонячно, саме тому я ношу блакитну сорочку. Що змушує вас думати, що заголовки не можуть читати люди?
кращий

29

Як хтось, хто написав кілька бібліотек для споживання REST-послуг, дозвольте мені дати вам уявлення клієнта про те, чому, на мою думку, обертання результату в метадані - це шлях:

  • Без загального підрахунку, як клієнт може знати, що він ще не отримав усього, що є, і повинен продовжувати підкачування через набір результатів? В інтерфейсі користувача, який не виконував перегляд на наступну сторінку, у гіршому випадку це може бути представлено як посилання Далі / Більше, яке насправді більше не отримувало даних.
  • Включення метаданих у відповідь дозволяє клієнту відстежувати менше стану. Тепер мені не потрібно узгоджувати свій запит REST з відповіддю, оскільки відповідь містить метадані, необхідні для реконструкції стану запиту (у цьому випадку курсор на набір даних).
  • Якщо стан є частиною відповіді, я можу виконувати кілька запитів до одного і того ж набору даних одночасно, і я можу обробляти запити в будь-якому порядку, коли вони випадково надходять, не в тому порядку, в якому я робив запити.

І пропозиція: Як і Twitter API , вам слід замінити номер_сторінки на прямий індекс / курсор. Причина в тому, що API дозволяє клієнту встановлювати розмір сторінки на запит. Чи повертається page_number - це кількість сторінок, які клієнт запитував до цього часу, або номер сторінки, що дала останній використовуваний розмір page_size (майже напевно пізніший, але чому б не уникнути такої двозначності взагалі)?


10
Для вашого першого маркера, чи було б підходящим рішенням пропустити посилання rel = next, якби не було наступної сторінки? На ваш другий маркер інформація все ще доступна у відповіді клієнту, вона просто не в тілі відповіді, а натомість у заголовках. +1 до вашого останнього абзацу.
Kyle Hayes

19

Я б рекомендував додати заголовки для тих самих. Переміщення метаданих у заголовки допомагає позбутися конвертів типу result, dataабо, recordsа тіло відповіді містить лише ті дані, які нам потрібні. Ви можете використовувати заголовок посилання, якщо ви також генеруєте посилання на пагінацію.

    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


2
Відповідно до RFC-6648, префікс "X-" повинен бути випущений у ключі метаданих.
Рей

1
@RayKoopa дякую, я оновив сторінку github, але забув оновити цю відповідь.
Аднан Камілі

0

просто додайте у ваш серверний API нову властивість у тіло відповіді. з прикладу .net core:

[Authorize]
[HttpGet]
public async Task<IActionResult> GetUsers([FromQuery]UserParams userParams)
{
  var users = await _repo.GetUsers(userParams);
  var usersToReturn = _mapper.Map<IEnumerable<UserForListDto>>(users);


  // create new object and add into it total count param etc
  var UsersListResult = new
  {
    usersToReturn,
    currentPage = users.CurrentPage,
    pageSize = users.PageSize,
    totalCount = users.TotalCount,
    totalPages = users.TotalPages
  };

  return Ok(UsersListResult);
}

У відповідь організму це виглядає так

{
"usersToReturn": [
    {
        "userId": 1,
        "username": "nancycaldwell@conjurica.com",
        "firstName": "Joann",
        "lastName": "Wilson",
        "city": "Armstrong",
        "phoneNumber": "+1 (893) 515-2172"
    },
    {
        "userId": 2,
        "username": "zelmasheppard@conjurica.com",
        "firstName": "Booth",
        "lastName": "Drake",
        "city": "Franks",
        "phoneNumber": "+1 (800) 493-2168"
    }
],
// metadata to pars in client side
"currentPage": 1,
"pageSize": 2,
"totalCount": 87,
"totalPages": 44

}


-3

загалом, я роблю простим способом, що завгодно, я створюю кінцеву точку restAPI, наприклад "localhost / api / method /: lastIdObtain /: countDateToReturn" із параметрами тези, ви можете зробити це простим запитом. в службі, напр. .net

jsonData function(lastIdObtained,countDatetoReturn){
'... write your code as you wish..'
and into select query make a filter
select top countDatetoreturn tt.id,tt.desc
 from tbANyThing tt
where id > lastIdObtained
order by id
}

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

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