Пейджинг у колекції відпочинку


134

Мені цікаво розкривати прямий інтерфейс REST до колекцій документів JSON (думаю, CouchDB або Persevere ). Проблема, з якою я стикаюся, полягає в тому, як керувати GETоперацією над коренем колекції, якщо колекція велика.

Як приклад робимо вигляд, що я розкриваю таблицю StackOverflow, Questionsде кожен рядок виставляється як документ (не те, що обов'язково є така таблиця, лише конкретний приклад значної колекції "документів"). Колекція буде доступна в /db/questionsзі звичайним CRUD апі GET /db/questions/XXX, PUT /db/questions/XXX, POST /db/questionsзнаходиться в грі. Стандартний спосіб отримати всю колекцію - це, GET /db/questionsале якщо це наївно скидає кожен рядок як об'єкт JSON, ви отримаєте досить значне завантаження та багато роботи з боку сервера.

Рішення - це, звичайно, пейджінг. Dojo вирішив цю проблему у своєму JsonRestStore за допомогою розумного розширення, сумісного з RFC2616, із застосуванням Rangeзаголовка із користувацьким блоком діапазону items. У результаті виходить 206 Partial Contentлише запитуваний діапазон. Перевага цього підходу над параметром запиту полягає в тому, що він залишає рядок запиту для ... запитів (наприклад, GET /db/questions/?score>200або дещо, і так, що було б закодовано %3E).

Цей підхід повністю охоплює поведінку, яку я хочу. Проблема полягає в тому, що RFC 2616 вказує, що на реакцію 206 (міна акценту):

Запит повинен бути включений в поле заголовка Range ( розділ 14.35 ) , яке вказує бажаний діапазон, і може включити поле заголовка If-Range ( розділ 14.27 ) , щоб зробити запит умовним.

Це має сенс у контексті стандартного використання заголовка, але це проблема, тому що я хотів би, щоб реакція 206 була за замовчуванням для обробки наївних клієнтів / випадкових людей, які досліджують.

Я детально переглянув RFC, шукаючи рішення, але був незадоволений своїми рішеннями і зацікавлений у прийнятті проблеми SO.

У мене були ідеї:

  • Повернення 200із Content-Rangeзаголовком! - Я не вважаю, що це неправильно, але я вважаю за краще, якщо більш очевидний показник того, що відповідь є лише частковим вмістом.
  • Повернення400 Range Required - не існує спеціального коду відповіді 400 для необхідних заголовків, тому помилка за замовчуванням повинна використовуватися і читатися вручну. Це також ускладнює пошук через веб-браузер (або якийсь інший клієнт, наприклад Resty).
  • Використовувати параметр запиту - Стандартний підхід, але я сподіваюся дозволити запити a la Persevere і це скоротить область простору імен запитів.
  • Просто повертайся 206! - Я думаю, що більшість клієнтів не злякаються, але я б краще не йшов проти ОБОВ'ЯЗКУ в RFC
  • Розширити специфікацію! Повернення266 Partial Content - поводиться точно так само, як 206, але відповідає на запит, який НЕ повинен містити Rangeзаголовка Я вважаю, що 266 є досить високим, що я не повинен стикатися з проблемами зіткнення, і для мене це має сенс, але мені не зрозуміло, вважається це табу чи ні.

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

Який найкращий спосіб відкрити повну колекцію через HTTP, коли колекція велика?


21
Нічого собі, це хороший приклад питання, коли раніше було зроблено серйозне мислення.
Хайко Рупп

можливий дублікат Пагинації у веб-додатку REST
rds

1
Що стосується підходу Доджо у використанні заголовка Діапазону, хоча Accept-Ranges дозволяє розширити, наскільки я можу сказати, EBNF для діапазону не: tools.ietf.org/html/rfc2616#section-14.35.2 . Специфікація вказує, Range = "Range" ":" ranges-specifierде останній у tools.ietf.org/html/rfc2616#section-14.35.1 описується лише як "специфікатор діапазонів байтів", який повинен починатися з "одиниці байтів", що визначається як рядок "байтів" ".
Бретт Замір

2
Content-RangeТема відноситься до тіла (можна використовувати із запитом при завантаженні великих файлів і т.д., або для відповіді при завантаженні). RangeЗаголовка використовується для запиту певного діапазону. Слід відповісти, 206коли Rangeзаголовок був включений у запит. Якщо цього не було, відповідь все ще може містити Content-Rangeзаголовок, але код відповіді повинен бути 200. Цей заголовок насправді здається ідеальним для підкачки.
Штійн де Вітт

Але сам RFC 2616 говорить, що "реалізація HTTP / 1.1 МОЖЕ ігнорувати діапазони, визначені за допомогою інших одиниць". Тож чи є хорошою практикою використовувати заголовки діапазону для пагинації? Тому що це може поставити під загрозу сумісність.
chetan choulwar

Відповіді:


23

Я відчуваю, що розширення діапазону HTTP не розроблені для вашого випадку використання, і тому ви не повинні намагатися. Часткова відповідь має на увазі 206, і вона 206повинна бути надіслана лише в тому випадку, якщо клієнт попросив її.

Ви можете розглянути інший підхід, наприклад, використання в Atom (де представлення за дизайном може бути частковим, і повертається зі статусом 200, а також потенційно посилаються на підключення ). Див. RFC 4287 та RFC 5005 .


14
Використання Dojo повністю в межах специфікації. Якщо сервер не розуміє itemsодиницю діапазону, він повертає повну відповідь. Я знайомий з Atom, але це не загальне рішення для пейджингів відпочинку. Це не є рішенням для одного випадку, більше того, яким має бути загальне рішення. Не всі документи / колекції відповідають моделі Atom, і немає ніяких причин змушувати її застосовувати, якщо цього не потрібно.
Карл Гертін

1
@KarlGuertin Погодився. Шкода, що це прийнята відповідь, тому що, здається, багато хто в громаді насправді підтримує Rangeі Content-Rangeз метою підкачки.
Штійн де Вітт

34

Я не дуже згоден з деякими з вас, хлопці. Я працював тижнями над цими можливостями для моєї служби REST. Що я в кінцевому підсумку робив, це дуже просто. Моє рішення має сенс лише для того, що REST люди називають колекцією.

Клієнт ОБОВ'ЯЗКОВО повинен включати заголовок "Діапазон", щоб вказати, яка частина колекції йому потрібна, або будь-яким іншим чином бути готовим обробити помилку 413 ЗАПИТАНОГО ВНУТРІШНЯ, коли запитувана колекція є занадто великою, щоб її можна було отримати за один обхід.

Сервер надсилає відповідь 206 ЧАСТИНИЙ ЗМІСТ із заголовком вмісту, вказуючи, яку частину ресурсу було надіслано, та заголовком ETag для ідентифікації поточної версії колекції. Я зазвичай використовую Facebook-подібний ETag {last_modification_timestamp} - {resource_id}, і я вважаю, що ETag колекції - це той самий недавно змінений ресурс, який він містить.

Щоб подати запит на конкретну частину колекції, клієнт ОБОВ'ЯЗКОВО повинен використовувати заголовок "Діапазон" та заповнити заголовок "Якщо збіг" ETag колекції, отриманий від раніше виконаних запитів на придбання інших частин тієї ж колекції. Тому сервер може перевірити, що колекція не змінилася, перш ніж надсилати запитувану частину. Якщо існує більш нова версія, відповідь 412 PRECONDITION FAILED повертається, щоб запросити клієнта отримати колекцію з нуля. Це необхідно, оскільки це може означати, що деякі ресурси могли бути додані або вилучені до або після запитуваної частини.

Я використовую ETag / If-Match в тандемі з Last-Modified / If-Unmodified-Since для оптимізації кешу. Браузери та проксі-сервери можуть розраховувати на один або обидва з них для своїх алгоритмів кешування.

Я думаю, що URL-адреса має бути чистою, якщо вона не включає запит пошуку / фільтра. Якщо ви подумаєте над цим, пошук - це не що інше, як часткове перегляд колекції. Замість автомобілів / пошуку? Q = тип URL-адреси BMW, ми повинні побачити більше автомобілів? Виробник = BMW.


Ви мали на увазі 416 "Запитаний діапазон не підлягає задоволенню" або "413" Запит об'єкта занадто великий?

1
@Mohamed Я думаю, ви маєте на увазі If-Unmodified-Since, що відповідає варіанту E-Tag If-Match, а не If-Modified-Since. Зважаючи на це, ви можете також розглянути можливість усунення цього обмеження, залежно від випадку використання. Скажімо, у вас є колекція, яка росте лише вгорі (як-от колекція стилів "найновіший перший"), найгірше, що може статися, якщо ця колекція зміниться між запитами, що користувач, який переглядає сторінки колекції, бачить записи двічі. (Що само по собі є також корисною інформацією. Він повідомляє користувачу, що колекція змінилася)
Євген Бересовський

20
413 - "Запити велику кількість об'єкта", а не "Запитаний об'єкт занадто великий". Це означає, що розмір вашого запиту, наприклад при завантаженні файлу, більший, ніж сервер готовий обробити. Тому використовувати його для цього не здається цілком доречним.
user247702

@Mohamed Я знаю, що це давнє питання, але якщо ETag колекції є ETag останнього зміненого ресурсу, який містить колекція, яке значення заголовка If-Match слід використовувати при зміні одного ресурсу в колекції? Використання значення ETag, поверненого разом із колекцією, є неправильним, оскільки клієнт зможе змінити ресурс, навіть якщо він не бачить останнього стану ресурсу.
Мікаель Марраш

8
Я категорично не згоден з використанням 413. Це код помилки, що означає, що клієнт надсилає те, що сервер відмовляється прийняти через розмір. Не навпаки! Див. Інструменти.ietf.org/html/rfc7231#section-6.5.11 (зауважте, що він говорить про завантаження корисного запиту . Не відповідне навантаження)!
ексгума

7

Ви все ще можете повернутися Accept-Rangesі Content-Rangesз 200кодом відповіді. Ці два заголовки відповідей дають вам достатньо інформації висновку тієї самої інформації, яку 206явно надає код відповіді.

Я б користувався Range для пагинації, і якби він просто повернув а 200для звичайної GET.

Це відчуває себе на 100% відпочинком і не ускладнює перегляд веб-сторінок.

Редагувати: Я написав про це запис у блозі: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html


5

Якщо відповідей декілька, і ви не хочете пропонувати всю колекцію одразу, чи означає це, що існує кілька варіантів?

На прохання /db/questionsповернутися 300 Multiple Choicesіз Linkзаголовками, які вказують, як дістатися до кожної сторінки, а також об’єктом JSON або HTML-сторінкою зі списком URL-адрес.

Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...

У вас буде по одному Linkзаголовку для кожної сторінки результатів (порожній рядок означає поточну URL-адресу, а URL-адреса однакова для кожної сторінки, щойно доступна для неї з різними діапазонами), а взаємозв'язок визначається як індивідуальний для наступної Linkспецифікації . Ці відносини пояснюють ваш звичай 266або ваше порушення 206. Ці заголовки є вашою машиночитаною версією, оскільки всі ваші приклади так чи інакше потребують розуміючого клієнта.

(Якщо ви дотримуєтесь маршруту "діапазон", я вважаю, що ваш власний 2xxкод повернення, як ви його описали, буде найкращою поведінкою тут. Очікується, що ви це зробите для своїх додатків, і такі ["коди статусу HTTP розширюються). "], і у вас є вагомі причини.)

300 Multiple Choicesкаже, що ви ДОЛЖНІ також надати органу спосіб вибору агента користувача. Якщо ваш клієнт розуміє, він повинен використовувати Linkзаголовки. Якщо це користувач, який переглядає користувач вручну, можливо, це HTML-сторінка із посиланнями на спеціальний "підказок" кореневого ресурсу, який може обробляти рендеринг цієї конкретної сторінки на основі URL-адреси? /humanpage/1/db/questionsчи щось таке огидне?


Коментар до публікації Річарда Левассера нагадує мені додатковий варіант: Acceptзаголовок (розділ 14.1). Коли я вийшов специфікацію oEmbed, я задумався, чому це було зроблено не повністю за допомогою HTTP, і написав альтернативну можливість їх використання.

Тримайте 300 Multiple Choices, на Linkзаголовки і сторінки HTML для вихідного наївним HTTP GET, але замість діапазонів використання, є нові відносини пейджінга визначають використання Acceptзаголовка. Ваш наступний HTTP-запит може виглядати приблизно так:

GET /db/questions HTTP/1.1
Host: paged.collection.example
Accept: application/json;PagingSpec=1.0;page=1

AcceptТема дозволяє визначити прийнятний тип контенту (ваш JSON повернення), а також розширюються параметри для даного типу (ваш номер сторінки). Опублікувавши мої нотатки з моєї записи на oEmbed (не можу посилання на неї тут, я перелічу це у своєму профілі), ви можете бути дуже чіткими і надати тут специфікацію / відношення версії, якщо вам потрібно буде переосмислити, що pageозначає цей параметр у майбутньому.


1
+1 заголовки посилань, але я також рекомендую загальні перші, попередні, наступні, останні релізи, а також попередній архів RFC5005, наступний архів та поточний.
Джозеф Холстен

> На запит до / db / questions поверніть 300 множинних варіантів із заголовками посилань, які вказують, як дістатися до кожної сторінки [..] Проблема з цим (і з найбільш чистою конструкцією REST) ​​полягає в тому, що це вбивство за затримку. Мета - мінімізувати мережеві запити. Цей перший запит повинен дати результати, а не посилання на більше запитів, які з часом дадуть нам потрібні дані.
Штійн де Вітт

4

Редагувати:

Подумавши про це трохи більше, я схильний погодитись, що заголовки діапазону не підходять для пагинації. За логікою, заголовок діапазону призначений для відповіді сервера, а не програми. Якщо ви обслуговували 100 мегабайт результатів, але сервер (або клієнт) міг обробляти лише 1 мегабайт одночасно, ну ось для чого потрібен заголовок Range.

Я також вважаю, що підмножина ресурсів - це власний ресурс (подібний до реляційної алгебри.), Тому він заслуговує на представлення в URL-адресі.

Отже, я заголосив свою оригінальну відповідь (нижче) про використання заголовка.


Я думаю, що ви відповіли на власне запитання, більш-менш - поверніть 200 або 206 із діапазоном вмісту та необов'язково використовуйте параметр запиту. Я б обнюхав агент користувача та тип вмісту і, залежно від них, перевірив би параметр запиту. В іншому випадку потрібні заголовки діапазону.

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

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

Звичайно, є також дискусії щодо того, чи має URL-адреса містити всі необхідні для подібного роду речі. Визначення діапазону за допомогою заголовків дехто може вважати "неспокійним".

Крім того, було б непогано, якби сервери могли відповісти заголовком "Can-Specify: Header1, header2", а веб-браузери представляли користувальницький інтерфейс, щоб користувачі могли заповнити значення за своїм бажанням.


Дякуємо за відповідь. Я думав над темою, але сподівався отримати другу думку. Якщо трапиться вказівник для аргументів заголовка?
Карл Гертін

Ось єдиний, який я заклав у закладки (див. Обговорення в коментарях): barelyenough.org/blog/2008/05/versioning-rest-web-services Ще один сайт розгорнувся навколо використання Ruby .json, .xml,. Незалежно від визначення тип вмісту запиту. Деякі приклади: * мова - якщо розмістити його в URL-адресі, це означає, що відправлення посилання в іншу країну призведе до неправильної мови. * пагинація - Якщо помістити його в шапку, це означає, що ви не можете пов’язати людей із тим, що бачите
Річард Левассер

* тип вмісту: поєднання проблем мови та сторінки, якщо вони є в URL-адресі, що робити, якщо клієнт не підтримує цей тип вмісту (наприклад, .ajax та розширення .html)? І навпаки, без цього типу вмісту в URL-адресі ви не можете забезпечити подання того ж представлення. "новий сайт ajax! example.com/cool.ajax" vs "крута стаття тут: example.com/article.ajax#id=123".
Річард Левассер

2
Незалежно від того, чи буде воно в URL-адресі чи ні, залежить від того, що це. Моє загальне правило: якщо він би ідентифікував конкретний ресурс (будь то ресурс у певному стані, підбір ресурсів чи дискретний результат), він іде в URL-адресі. Пошукові запити, сторінки та сторінки спокійних транзакцій - хороші приклади цього. Якщо це щось необхідне для перетворення абстрактного уявлення в конкретне уявлення, воно йде в заголовку. авторська інформація та тип вмісту - хороші приклади цього.
Річард Левассер

Я вважаю рядок запиту в URL-адресі як варіант запиту ресурсу, який задається.
wprl

3

Ви можете розглянути можливість використання такої моделі, як протокол Atom Feed, оскільки в ній є нормальна HTTP-модель колекцій і як ними маніпулювати (де божевільне означає WebDAV).

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

Перехід від Atom XML до вмісту JSON не повинен впливати на ідею.


3

Я думаю, що справжня проблема полягає в тому, що в специфікації немає нічого, що б нам говорило, як робити автоматичні переадресації, стикаючись з 413 - запитувана особа занадто велика.

Я нещодавно боровся з цією самою проблемою, і натхнення шукав у книзі RESTful Web Services . Особисто я не вважаю 206 підходящим через вимогу заголовка. Мої думки також привели мене до 300, але я подумав, що це більше для різних типів міми, тому я подивився, що Річардсон і Рубі повинні сказати з цього приводу в Додатку В, сторінка 377. Вони припускають, що сервер просто вибрати бажаний представлення і відправити його назад з 200, в основному ігноруючи поняття, що це повинно бути 300.

Це також джебс з поняттям посилань на наступні ресурси, які ми маємо від атома. Я реалізував рішення - додати ключі "наступний" та "попередній" до карти json, яку я надсилав назад, і зробити це з нею.

Пізніше я почав думати, можливо, це те, що потрібно зробити, це надіслати 307 - Тимчасовий перенаправлення на посилання, яке було б на кшталт / db / questions / 1,25 - що залишає оригінальний URI як канонічне ім'я ресурсу, але воно змушує вас відповідний ім'я підпорядкованого ресурсу. Таку поведінку я хотів би побачити з 413, але 307 здається хорошим компромісом. Насправді ще не пробував цього в коді. Що ще краще - перенаправлення на переадресацію до URL-адреси, що містить фактичні ідентифікатори останніх запитань. Наприклад, якщо кожне запитання має цілий ідентифікатор, а в системі є 100 питань, і ви хочете показати десять останніх, запитів на / db / questions повинно бути 307'd / db / questions / 100,91

Це дуже гарне запитання, дякую, що його задали. Ви підтвердили для мене, що я не горіх за те, що витратив дні на це, думаючи.


303 було б кращим у цьому плані, ніж 307. 307 означає, що початкова URL-адреса незабаром почне реагувати, як очікує клієнт.
Ніколас Шенкс

RFC 7231 згадує код статусу HTTP 413 як занадто великий корисний навантаження і пов'язує цей код із розміром запиту, а не з можливим розміром відповіді.
beawolf

1

Ви можете виявити Rangeзаголовок і імітувати Dojo, якщо він присутній, і імітувати Atom, якщо його немає. Мені здається, що це акуратно розділяє випадки використання. Якщо ви відповідаєте на запит REST з вашої програми, ви очікуєте, що він буде відформатований із Rangeзаголовком. Якщо ви відповідаєте на випадковий веб-переглядач, то, якщо ви повернете посилання для підкачки, це дозволить інструменту простий спосіб вивчити колекцію.


1

Однією з найбільших проблем із заголовками діапазону є те, що багато корпоративних проксі-серверів фільтрують їх. Я б радив використовувати замість цього параметр запиту.



0

Мені здається, що найкращий спосіб зробити це - включити діапазон як параметри запиту. наприклад, GET / db / questions /? date> mindate & date <maxdate . Після отримання на сторінку / db / questions / без параметрів запиту, поверніть 303 з Location: / db / questions /? Query-settings-to-retrieve-the-default-page . Потім надайте іншу URL-адресу, за якою користувач використовує ваш API, щоб отримати статистику про колекцію (наприклад, які параметри запиту використовувати, якщо він / він хоче всю колекцію);


0

Хоча можливо використовувати для цього заголовок діапазону, я не думаю, що це було наміром. Здається, він був розроблений для обробки нестійких з'єднань, а також для обмеження даних (тому клієнт може запитувати частину запиту, якщо чогось не було або розмір був занадто великий для обробки). Ви зламаєте сторінку на щось, що, ймовірно, використовується для інших цілей на рівні зв'язку. "Правильний" спосіб обробки сторінок - це типи, які ви повертаєте. Замість того, щоб повертати об’єкт запитань, вам слід повернути новий тип.

Тож якщо питання такі:

<questions> <question index=1></question> <question index=2></question> ... </questions>

Новий тип може бути приблизно таким:

<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>

Звичайно, ви керуєте своїми типами медіа, щоб ви могли зробити свої "сторінки" форматом, який відповідає вашим потребам. Якщо ви робите щось загальне, ви можете мати один аналізатор на клієнті, щоб обробляти підкачки однаковими для всіх типів. Я думаю, що це більше відповідає духу специфікації HTTP, а не підробляє параметр Range для чогось іншого.

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