Пейджинг в коллекции отдыха

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

В качестве примера притворяюсь, что я показываю таблицу StackOverflow Questions, где каждая строка экспонируется как документ (не обязательно, чтобы такая таблица была всего лишь конкретным примером значительного набора "документов" ). Коллекция будет доступна в /db/questions с обычным CRUD api GET /db/info/XXX, PUT /db/info/XXX, POST /db/questions. Стандартный способ получить всю коллекцию - GET /db/questions, но если это наивно сбрасывает каждую строку как объект JSON, вы получите довольно значительную загрузку и большую часть работы со стороны сервера.

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

Этот подход полностью охватывает поведение, которое я хочу. Проблема в том, что RFC 2616 указывает, что в ответе 206 (акцент мой):

запрос ДОЛЖЕН включить поле заголовка диапазона (раздел 14.35)   указывая желаемый диапазон, и МОЖЕТ включать в себя диапазон If   (раздел 14.27), чтобы сделать запрос условным.

Это имеет смысл в контексте стандартного использования заголовка, но является проблемой, потому что я хотел бы, чтобы ответ 206 был по умолчанию обрабатывать наивные клиенты/случайные люди, изучающие.

Я подробно рассмотрел RFC, ища решение, но был недоволен моими решениями, и я заинтересован в SO, чтобы решить эту проблему.

Идеи, которые у меня были:

  • Верните 200 с заголовком Content-Range! - Я не думаю, что это неправильно, но я бы предпочел, чтобы более очевидный индикатор того, что ответ - это только частичный контент.
  • Возврат 400 Range Required - для требуемых заголовков не существует специального кода ответа 400, поэтому ошибка по умолчанию должна использоваться и считываться вручную. Это также затрудняет исследование через веб-браузер (или какой-либо другой клиент, такой как Resty).
  • Использовать параметр запроса - стандартный подход, но я надеюсь разрешить запросы a la Persevere, и это сокращает пространство имен запросов.
  • Просто верните 206! - Я думаю, что большинство клиентов не будет волноваться, но я бы предпочел не идти против MUST в RFC.
  • Расширьте спецификацию! Return 266 Partial Content - ведет себя точно так же, как 206, но отвечает на запрос, который НЕ ДОЛЖЕН содержать заголовок Range. Я полагаю, что 266 достаточно высоко, чтобы я не сталкивался с проблемами столкновения, и это имеет смысл для меня, но я не понимаю, считается ли это табу или нет.

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

Каков наилучший способ опубликовать полную коллекцию через HTTP при большой коллекции?

Ответ 1

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

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

Ответ 2

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

Клиент ДОЛЖЕН включать заголовок "Range", чтобы указать, какая часть коллекции ему нужна, или, в противном случае, быть готовой обработать ошибку 413 REQUESTED ENTITY TOO LARGE, когда запрошенная коллекция слишком велика, чтобы ее можно было получить за один раз туда-обратно.

Сервер отправляет ответ 206 PARTIAL CONTENT с заголовком Content-Range, определяющим, какая часть ресурса отправлена, и заголовок ETag для идентификации текущей версии коллекции. Обычно я использую Facebook-подобный ETag {last_modification_timestamp} - {resource_id}, и я считаю, что ETag коллекции - это тот самый последний измененный ресурс, который он содержит.

Чтобы запросить определенную часть коллекции, клиент ДОЛЖЕН использовать заголовок "Range" и заполнить заголовок "If-Match" с помощью ETag коллекции, полученной из ранее выполненных запросов, для приобретения других частей той же коллекции, Поэтому сервер может проверить, что коллекция не изменилась перед отправкой запрошенной части. Если существует более новая версия, возвращается ответ 412 PRECONDITION FAILED, чтобы предложить клиенту получить сборку с нуля. Это необходимо, потому что это может означать, что некоторые ресурсы могли быть добавлены или удалены до или после запрашиваемой части.

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

Я думаю, что URL-адрес должен быть чистым, если только он не включает запрос поиска/фильтра. Если вы думаете об этом, поиск - не что иное, как частичный вид коллекции. Вместо автомобилей/поиска? Q = тип URL-адресов BMW, мы должны увидеть больше автомобилей? Производитель = BMW.

Ответ 3

Если существует более одной страницы ответов, и вы не хотите предлагать всю коллекцию сразу, значит ли это, что есть несколько вариантов?

В запросе /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), а также расширяемые параметры для этого типа (номер вашей страницы). Riffing по моим заметкам из моей записи oEmbed (не могу ссылаться на нее здесь, я перечислил ее в своем профиле), вы можете быть очень явным и предоставить версию spec/relation здесь, если вам нужно переопределить, что page в будущем.

Ответ 4

Вы можете вернуть Accept-Ranges и Content-Ranges с кодом ответа 200. Эти два заголовка ответа дают вам достаточно информации, чтобы вывести ту же информацию, что и код ответа 206 явно.

Я использовал бы Range для разбивки на страницы и просто вернул бы 200 для простого GET.

Это ощущение 100% RESTful и не делает просмотр более трудным.

Изменить: Я написал сообщение в блоге об этом: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

Ответ 5

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

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

Переход от Atom XML к содержимому JSON не должен влиять на идею.

Ответ 6

Edit:

Подумав об этом чуть больше, я склонен согласиться с тем, что заголовки Range не подходят для разбивки на страницы. Логика состоит в том, что заголовок Range предназначен для ответа сервера, а не приложений. Если вы подали 100 мегабайт результатов, но сервер (или клиент) мог обрабатывать только 1 мегабайт за раз, ну, для чего нужен заголовок Range.

Я также считаю, что подмножество ресурсов - это собственный ресурс (аналогичный реляционной алгебре.), поэтому он заслуживает представления в URL-адресе.

В основном, я отказываюсь от своего первоначального ответа (ниже) об использовании заголовка.


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

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

Вы можете просто предоставить им специальный клиент в зависимости от запроса - если он похож на простой браузер, отправьте небольшое приложение ajax, которое отображает страницу и устанавливает необходимые заголовки.

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

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

Ответ 7

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

Недавно я боролся с этой проблемой, и я искал вдохновения в книге RESTful Web Services. Лично я не думаю, что 206 уместно из-за требования заголовка. Мои мысли также привели меня к 300, но я думал, что это больше подходит для разных типов mime, поэтому я посмотрел, что Ричардсон и Руби должны были сказать по этому вопросу в Приложении B, стр. 377. Они предлагают, чтобы сервер просто выбирал предпочтительный представление и отправить его с 200, в основном игнорируя представление о том, что оно должно быть 300.

Это также смещается с понятием ссылок на следующие ресурсы, которые мы имеем от атома. Реализованное мной решение заключалось в том, чтобы добавить "следующий" и "предыдущий" ключи к json-карте, которую я отправлял обратно, и делать с ней.

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

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

Ответ 8

Вы можете обнаружить заголовок Range и mimic Dojo, если он присутствует, и имитировать Atom, если это не так. Мне кажется, что это аккуратно делит варианты использования. Если вы отвечаете на запрос REST из вашего приложения, вы ожидаете, что он будет отформатирован с заголовком Range. Если вы отвечаете на случайный браузер, то, если вы вернете пейджинговые ссылки, это позволит инструменту обеспечить простой способ изучения коллекции.

Ответ 10

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

Ответ 11

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

Ответ 12

Хотя для этой цели можно использовать заголовок Range, я не думаю, что это было целью. Кажется, он был разработан для обработки нечетких соединений, а также для ограничения данных (поэтому клиент может запросить часть запроса, если что-то отсутствует или размер слишком велик для обработки). Вы взламываете разбивку на страницы во что-то, что, вероятно, используется для других целей на уровне коммуникации. "Правильный" способ обработки разбиения на страницы - это типы, которые вы возвращаете. Вместо того, чтобы возвращать объект вопросов, вы должны вместо этого возвращать новый тип.

Итак, если вопросы таковы:

<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 для чего-то еще.