Рекомендации по разбиению на страницы API


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

Как и многие API, этот разбивает на страницы большие результаты. Если вы запросите /foos, вы получите 100 результатов (т. е. foo #1-100) и ссылку на /foos?Страница=2, которая должна вернуть foo #101-200.

к сожалению, если foo #10 удаляется из набора данных до того, как потребитель API сделает следующий запрос, /foos?page=2 будет смещен на 100 и вернет foos #102-201.

Это проблема для потребителей API, которые пытаются вытащить все foos-они не получат foo #101.

каков наилучший способ справиться с этим? Мы хотели бы сделать его максимально легким (т. е. избежать обработки сеансов для запросов API). Примеры из других API будут очень признательны!

10 232

10 ответов:

Я не совсем уверен, как обрабатываются ваши данные, так что это может или не может работать, но вы рассматривали разбиение на страницы с полем timestamp?

при запросе /foos вы получаете 100 результатов. Затем ваш API должен возвращать что-то вроде этого (предполагая JSON, но если ему нужен XML, можно следовать тем же принципам):

{
    "data" : [
        {  data item 1 with all relevant fields    },
        {  data item 2   },
        ...
        {  data item 100 }
    ],
    "paging":  {
        "previous":  "http://api.example.com/foo?since=TIMESTAMP1" 
        "next":  "http://api.example.com/foo?since=TIMESTAMP2"
    }

}

просто заметка, только с помощью одной метки времени зависит от неявного "предела" в ваших результатах. Вы можете добавить явное ограничение или использовать until собственность.

метка времени может быть динамически определяется с помощью последнего элемента данных в списке. Это, кажется, более или менее, как Facebook страницы в его Graph API (прокрутите вниз до конца, чтобы увидеть ссылки на страницы в формате, который я дал выше).

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

у вас есть несколько проблем.

во-первых, у вас есть пример, который вы привели.

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

Если вы не привязываете исходный набор данных, то это просто факт жизни.

вы можете заставить пользователя сделать явный снимок:

POST /createquery
filter.firstName=Bob&filter.lastName=Eubanks

что результаты:

HTTP/1.1 301 Here's your query
Location: http://www.example.org/query/12345

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

Если случай использования просто, что ваши пользователи хотят (и нужно) все данные, то вы можете просто дать им:

GET /query/12345?all=true

и просто отправить весь комплект.

Если у вас есть разбиение на страницы, вы также сортируете данные по некоторому ключу. Почему бы не позволить клиентам API включить ключ последнего элемента ранее возвращенной коллекции в URL и добавить WHERE предложение к вашему SQL-запросу (или что-то эквивалентное, если вы не используете SQL), чтобы он возвращал только те элементы, для которых ключ больше этого значения?

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

подход 1: когда сервер недостаточно умен, чтобы обрабатывать состояния объектов.

вы можете отправить все уникальные идентификаторы кэшированных записей на сервер, например ["id1","id2","id3","id4","id5","id6","id7","id8","id9", "id10"] и логический параметр, чтобы узнать, запрашиваете ли вы новые записи(потяните для обновления) или старые записи(загрузите больше).

ваш Север должен нести ответственность, чтобы вернуться новые записи (загрузите больше записей или новые записи через pull для обновления), а также идентификаторы удаленных записей из ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"].

пример: Если вы хотите загрузить больше, то ваш запрос должен выглядеть примерно так:-

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"]
}

теперь предположим, что вы запрашиваете старые записи (загрузите больше) и предположим, что запись " id2 "обновляется кем-то, а записи" id5 " и " id8 " удаляются с сервера, а затем ваш сервер ответ должен выглядеть примерно так:-

{
        "records" : [
{"id" :"id2","more_key":"updated_value"},
{"id" :"id11","more_key":"more_value"},
{"id" :"id12","more_key":"more_value"},
{"id" :"id13","more_key":"more_value"},
{"id" :"id14","more_key":"more_value"},
{"id" :"id15","more_key":"more_value"},
{"id" :"id16","more_key":"more_value"},
{"id" :"id17","more_key":"more_value"},
{"id" :"id18","more_key":"more_value"},
{"id" :"id19","more_key":"more_value"},
{"id" :"id20","more_key":"more_value"}],
        "deleted" : ["id5","id8"]
}

но в этом случае, если у вас много локальных кэшированных записей, предположим, 500, то ваша строка запроса будет слишком длинной, как это: -

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10",………,"id500"]//Too long request
}

подход 2: когда сервер достаточно умен, чтобы обрабатывать состояния объектов в соответствии с датой.

вы можете отправить идентификатор первой записи и последней записи и предыдущего запроса эпохи времени. Таким образом, ваш запрос всегда мал, даже если у вас есть большая сумма из кэшированных записей

пример: Если вы хотите загрузить больше, то ваш запрос должен выглядеть примерно так:-

{
        "isRefresh" : false,
        "firstId" : "id1",
        "lastId" : "id10",
        "last_request_time" : 1421748005
}

ваш сервер отвечает за возврат идентификаторов удаленных записей, которые удаляются после last_request_time, а также возвращает обновленную запись после last_request_time между "id1" и "id10" .

{
        "records" : [
{"id" :"id2","more_key":"updated_value"},
{"id" :"id11","more_key":"more_value"},
{"id" :"id12","more_key":"more_value"},
{"id" :"id13","more_key":"more_value"},
{"id" :"id14","more_key":"more_value"},
{"id" :"id15","more_key":"more_value"},
{"id" :"id16","more_key":"more_value"},
{"id" :"id17","more_key":"more_value"},
{"id" :"id18","more_key":"more_value"},
{"id" :"id19","more_key":"more_value"},
{"id" :"id20","more_key":"more_value"}],
        "deleted" : ["id5","id8"]
}

Потяните, Чтобы Обновить:-

enter image description here

загрузить Еще

enter image description here

Это может быть трудно найти лучшие практики, так как большинство систем С API не приспособлены для этого сценария, потому что это крайний край, или они обычно не удаляют записи (Facebook, Twitter). Facebook фактически говорит, что каждая "страница" может не иметь количества запрошенных результатов из-за фильтрации, выполненной после разбиения на страницы. https://developers.facebook.com/blog/post/478/

Если вам действительно нужно разместить этот крайний случай, вам нужно "вспомнить", где вы остановились. предложение jandjorgensen-это просто место, но я бы использовал поле, которое гарантированно будет уникальным, как первичный ключ. Возможно, потребуется использовать несколько полей.

следуя потоку Facebook, вы можете (и должны) кэшировать уже запрошенные страницы и просто возвращать те, у которых удаленные строки отфильтрованы, если они запрашивают страницу, которую они уже запросили.

разбиение на страницы обычно является" пользовательской " операцией и для предотвращения перегрузки как на компьютерах, так и в человеческом мозге вы обычно даете подмножество. Однако, вместо того, чтобы думать, что мы не получаем весь список, может быть, лучше спросить имеет ли это значение?

Если требуется точное представление прокрутки в реальном времени, API REST, которые по своей природе являются запросом/ответом, не очень хорошо подходят для этой цели. Для этого вы должны рассмотреть WebSockets или HTML5 Server-Sent события, чтобы ваш фронт конец знать, когда дело с изменениями.

Если есть нужно чтобы получить снимок данных, я бы просто предоставил вызов API, который предоставляет все данные в одном запросе без разбиения на страницы. Имейте в виду, вам понадобится что-то, что будет делать потоковую передачу вывода без временной загрузки его в память, если у вас есть большой набор данных.

для моего случая я неявно назначаю некоторые вызовы API, чтобы позволить получить всю информацию (в первую очередь ссылочную таблицу данные.) Вы также можете защитить эти API, так что это не повредит вашей системе.

Я думаю, что в настоящее время ваш api на самом деле отвечает так, как он должен. Первые 100 записей на странице в общем порядке объекты, которые вы поддерживаете. Ваше объяснение говорит, что вы используете какие-то идентификаторы порядка для определения порядка ваших объектов для разбиения на страницы.

теперь, если вы хотите, чтобы Страница 2 всегда начиналась со 101 и заканчивалась на 200, то вы должны сделать количество записей на странице как переменную, так как они подлежат исключение.

вы должны сделать что-то вроде ниже псевдокоде:

page_max = 100
def get_page_results(page_no) :

    start = (page_no - 1) * page_max + 1
    end = page_no * page_max

    return fetch_results_by_id_between(start, end)

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

ваш пример удаляемого элемента - это только верхушка айсберга. Что делать, если вы фильтруете по color=blue но кто-то меняет цвет В между запросами? Извлечение всех элементов постраничным способом надежно - это невозможно... если... мы реализуем история версий.

я реализовал его, и это на самом деле менее сложно, чем я ожидал. Вот что я сделал:

  • я создал одну таблицу changelogs С автоинкрементного столбца ID
  • мои сущности имеют id поле, но это не первичный ключ
  • сущности имеют changeId поле, которое является как первичным ключом, так и внешним ключом для подменышей.
  • всякий раз, когда пользователь создает, обновляет или удаляет запись, система вставляет новую запись в changelogs, захватывает идентификатор и присваивает его новая версия сущности, которую он затем вставляет в БД
  • мои запросы выберите максимальный changeId (сгруппированный по id) и самостоятельно присоединитесь к нему, чтобы получить самые последние версии всех записей.
  • фильтры применяются к самым последним записям
  • в поле "состояние" отслеживает будет ли элемент удален
  • max changeId возвращается клиенту и добавляется в качестве параметра запроса в последующих запросах
  • потому что только новые изменения создаются, каждый changeId представляет собой уникальный снимок базовых данных на момент создания изменения.
  • это означает, что вы можете кэшировать результаты запросов, параметр changeId в них навсегда. Результаты никогда не истечет, потому что они никогда не будут изменение.
  • это также открывает захватывающую функцию, такую как откат / возврат, синхронизация кэша клиента и т. д. Любые функции, которые извлекают выгоду из истории изменений.

вариант A: набор ключей пагинации с меткой времени

во избежание недостатков смещения пагинации вы упомянули, Вы можете использовать набор ключей на основе пагинации. Обычно сущности имеют метку времени, которая указывает время их создания или изменения. Эта временная метка может использоваться для разбиения на страницы: просто передайте временную метку последнего элемента в качестве параметра запроса для следующего запроса. Сервер, в свою очередь, использует метку времени в качестве критерия фильтрации (например, WHERE modificationDate >= receivedTimestampParameter)

{
    "elements": [
        {"data": "data", "modificationDate": 1512757070}
        {"data": "data", "modificationDate": 1512757071}
        {"data": "data", "modificationDate": 1512757072}
    ],
    "pagination": {
        "lastModificationDate": 1512757072,
        "nextPage": "https://domain.de/api/elements?modifiedSince=1512757072"
    }
}

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

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

вы можете сделать эти недостатки менее вероятно путем увеличения размера страницы и использования временных меток с точностью до миллисекунды.

вариант Б: расширенный набор страниц с продолжением маркер

чтобы справиться с упомянутыми недостатками обычного разбиения на страницы набора ключей, вы можете добавить смещение к метке времени и использовать так называемый "токен продолжения" или "курсор". Смещение позиции элемента относительно первого элемента в той же метки. Как правило, маркер имеет формат Timestamp_Offset. Это переходит к клиенту в ответ и может быть отправлен обратно на сервер для получения следующей страницы.

{
    "elements": [
        {"data": "data", "modificationDate": 1512757070}
        {"data": "data", "modificationDate": 1512757072}
        {"data": "data", "modificationDate": 1512757072}
    ],
    "pagination": {
        "continuationToken": "1512757072_2",
        "nextPage": "https://domain.de/api/elements?continuationToken=1512757072_2"
    }
}

маркер " 1512757072_2 "указывает на последний элемент страницы и указывает"клиент уже получил второй элемент с меткой времени 1512757072". Таким образом, сервер знает, где продолжить.

Пожалуйста, обратите внимание, что вы должны обрабатывать тех случаях, когда элементы были изменены между двумя запросами. Это обычно делается путем добавления контрольной суммы к знак. Эта контрольная сумма вычисляется по идентификаторам всех элементов с этой меткой времени. Таким образом, мы получаем такой формат токенов: Timestamp_Offset_Checksum.

для получения дополнительной информации об этом подходе проверьте сообщение в блоге"разбиение на страницы Web API с токенами продолжения". Недостатком этого подхода является сложная реализация, поскольку есть много угловых случаев, которые необходимо учитывать. Вот почему библиотеки любят продолжение-маркер может быть удобно (если вы использование языка Java / A JVM). Отказ от ответственности: я являюсь автором поста и соавтором библиотеки.

просто добавить к этому ответу Камилк:https://www.stackoverflow.com/a/13905589

во многом зависит от того, насколько большой набор данных, с которым вы работаете. Небольшие наборы данных эффективно работают на смещение пагинации но большие наборы данных в реальном времени требуют разбиение курсора на страницы.

нашел замечательную статью о том, как Slack эволюционировали его API-страницы, поскольку там наборы данных увеличились, объясняя плюсы и минусы на каждом этапе : https://slack.проектирование / эволюция-api-pagination-at-slack-1c1f644f8e12