Лучшие практики

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

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

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

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

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

Ответ 1

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

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

Ответ 2

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

Сначала у вас есть пример, который вы указали.

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

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

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

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

Какие результаты:

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

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

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

GET /query/12345?all=true

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

Ответ 3

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

Ответ 4

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

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

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

Ваш сервер должен отвечать за возврат новых записей (загрузка большего количества записей или новых записей с помощью pull для обновления), а также идентификаторы удаленных записей из [ "id1" , "id2" , "id3", "id4", "id5", "ID6", "ИД7", "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"]
}

Pull To Refresh: -

enter image description here

Загрузить больше

enter image description here

Ответ 5

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

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

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

Ответ 6

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

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

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

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

Ответ 7

Вариант 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"
    }
}

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

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

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

Вариант B: расширенная нумерация клавиш с помощью токена продолжения

Чтобы справиться с упомянутыми недостатками обычной нумерации клавиш, вы можете добавить смещение к отметке времени и использовать так называемый "токен продолжения" или "курсор". Смещение - это позиция элемента относительно первого элемента с такой же отметкой времени. Обычно токен имеет такой формат, как 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.

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

Ответ 8

Я думаю, что в настоящее время ваш 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)

Ответ 9

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

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

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

  • Я создал отдельную таблицу changelogs с столбцом идентификатора с автоматическим увеличением
  • Мои объекты имеют поле id, но это не первичный ключ
  • Объекты имеют поле changeId, которое является как первичным ключом, так и внешним ключом для изменений.
  • Всякий раз, когда пользователь создает, обновляет или удаляет запись, система вставляет новую запись в changelogs, захватывает идентификатор и присваивает ему новую версию объекта, которая затем вставляет в DB
  • Мои запросы выбирают максимальное changeId (сгруппировано по id) и самосоединяются, чтобы получить самые последние версии всех записей.
  • Фильтры применяются к самым последним записям
  • Поле состояния отслеживает, удален ли элемент.
  • Max changeId возвращается клиенту и добавляется в качестве параметра запроса в последующих запросах.
  • Поскольку создаются только новые изменения, каждый changeId представляет собой уникальный моментальный снимок базовых данных в момент создания изменения.
  • Это означает, что вы можете кэшировать результаты запросов с параметром changeId в них навсегда. Результаты никогда не истекут, потому что они никогда не изменятся.
  • Это также открывает захватывающие функции, такие как откат/возврат, синхронизация кеша клиента и т.д. Любые функции, которые извлекают выгоду из истории изменений.

Ответ 10

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

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

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