Práticas recomendadas de paginação da API

288

Eu adoraria alguma ajuda para lidar com um caso estranho com uma API paginada que estou construindo.

Como muitas APIs, esta pagina grandes resultados. Se você consultar / foos, obterá 100 resultados (por exemplo, foo # 1-100) e um link para / foos? Page = 2, que deve retornar foo # 101-200.

Infelizmente, se o foo # 10 for excluído do conjunto de dados antes que o consumidor da API faça a próxima consulta, / foos? Page = 2 será compensado em 100 e retornará o foos # 102-201.

Este é um problema para os consumidores de API que estão tentando obter todos os foos - eles não receberão o foo 101.

Qual é a melhor prática para lidar com isso? Gostaríamos de torná-lo o mais leve possível (ou seja, evitando sessões de manipulação para solicitações de API). Exemplos de outras APIs seriam muito apreciados!

2arrs2ells
fonte
1
qual é o problema aqui? parece ok para mim, de qualquer maneira o usuário receberá 100 itens.
NARKOZ
2
Estou enfrentando esse mesmo problema e procurando uma solução. AFAIK, não há realmente um mecanismo sólido garantido para fazer isso, se cada página executar uma nova consulta. A única solução em que consigo pensar é manter uma sessão ativa e manter o conjunto de resultados no lado do servidor e, em vez de executar novas consultas para cada página, basta pegar o próximo conjunto de registros em cache.
Jerry rodeio
31
Dê uma olhada em como o Twitter conseguir isso dev.twitter.com/rest/public/timelines
java_geek
1
@java_geek Como o parâmetro since_id é atualizado? Na página do twitter, parece que eles estão fazendo as duas solicitações com o mesmo valor para since_id. Gostaria de saber quando será atualizado para que, se forem adicionados tweets mais recentes, eles possam ser contabilizados?
Petar
1
@Petar O parâmetro since_id precisa ser atualizado pelo consumidor da API. Se você ver, o exemplo não se refere aos clientes processamento de tweets
java_geek

Respostas:

175

Não tenho certeza de como seus dados são manipulados, portanto, isso pode ou não funcionar, mas você já pensou em paginar com um campo de carimbo de data / hora?

Quando você consulta / foos, obtém 100 resultados. Sua API deve retornar algo assim (assumindo JSON, mas se precisar de XML, os mesmos princípios podem ser seguidos):

{
    "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"
    }

}

Apenas uma observação: o uso de apenas um carimbo de data e hora depende de um 'limite' implícito nos seus resultados. Você pode adicionar um limite explícito ou também usar uma untilpropriedade.

O registro de data e hora pode ser determinado dinamicamente usando o último item de dados na lista. Parece ser mais ou menos como o Facebook pagina em sua API do Graph (role para baixo até o final para ver os links de paginação no formato que eu dei acima).

Um problema pode ser se você adicionar um item de dados, mas com base na sua descrição, parece que eles seriam adicionados ao final (se não, avise-me e verei se posso melhorar isso).

ramblinjan
fonte
29
Não é garantido que os carimbos de data e hora sejam únicos. Ou seja, vários recursos podem ser criados com o mesmo registro de data e hora. Portanto, essa abordagem tem o lado negativo de que a próxima página pode repetir as últimas (poucas?) Entradas da página atual.
rublo
4
@prmatta Na verdade, dependendo da implementação do banco de dados, é garantido que um carimbo de data e hora seja único .
Ramblinjan
2
@jandjorgensen No seu link: "O tipo de dados do registro de data e hora é apenas um número incremental e não preserva uma data ou hora. ... No SQL Server 2008 e posterior, o tipo de registro de data e hora foi renomeado para versão de linha , presumivelmente para refletir melhor sua propósito e valor ". Portanto, não há evidências aqui de que os carimbos de data / hora (aqueles que realmente contêm um valor de tempo) sejam únicos.
Nolan Amy
3
@jandjorgensen Gostei da sua proposta, mas você não precisaria de algum tipo de informação nos links de recursos, para sabermos se vamos para a próxima ou para a próxima? Sth like: "previous": " api.example.com/foo?before=TIMESTAMP " "next": " api.example.com/foo?since=TIMESTAMP2 " Também usaríamos nossos IDs de sequência em vez de um carimbo de data / hora. Você vê algum problema com isso?
longliveenduro
5
Outra opção semelhante é usar o campo de cabeçalho link especificado no RFC 5988 (seção 5): tools.ietf.org/html/rfc5988#page-6
Anthony F
28

Você tem vários problemas.

Primeiro, você tem o exemplo que você citou.

Você também tem um problema semelhante se as linhas forem inseridas, mas, neste caso, o usuário obtém dados duplicados (sem dúvida, mais fáceis de gerenciar do que com dados ausentes, mas ainda assim um problema).

Se você não estiver capturando instantaneamente o conjunto de dados original, isso é apenas um fato da vida.

Você pode fazer com que o usuário faça um instantâneo explícito:

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

Quais resultados:

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

Em seguida, você pode paginar isso o dia inteiro, pois agora está estático. Isso pode ser razoavelmente leve, já que você pode capturar as chaves do documento real em vez das linhas inteiras.

Se o caso de uso for simplesmente o de que seus usuários desejam (e precisam) todos os dados, você pode simplesmente fornecer a eles:

GET /query/12345?all=true

e apenas envie o kit inteiro.

Will Hartung
fonte
1
(Classificação padrão de Foos é por data de criação, de modo inserção da linha não é um problema.)
2arrs2ells
Na verdade, capturar apenas chaves de documento não é suficiente. Dessa forma, você precisará consultar os objetos completos por ID quando o usuário os solicitar, mas pode ser que eles não existam mais.
Scadge
27

Se você tiver paginação, também classifique os dados por alguma chave. Por que não permitir que os clientes da API incluam a chave do último elemento da coleção retornada anteriormente na URL e adicionem uma WHEREcláusula à sua consulta SQL (ou algo equivalente, se você não estiver usando o SQL), para que ela retorne apenas os elementos para os quais a chave é maior que esse valor?

kamilk
fonte
4
Esta não é uma sugestão ruim, no entanto, apenas porque você classifica por um valor não significa que seja uma 'chave', ou seja, única.
Chris Peacock
Exatamente. Por exemplo, no meu caso, o campo de classificação passa a ser uma data e está longe de ser único.
precisa saber é o seguinte
19

Pode haver duas abordagens, dependendo da lógica do servidor.

Abordagem 1: quando o servidor não é inteligente o suficiente para lidar com estados de objetos.

Você pode enviar todos os IDs exclusivos de registro em cache para o servidor, por exemplo ["id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8", "id9", "id10"] e um parâmetro booleano para saber se você está solicitando novos registros (puxe para atualizar) ou registros antigos (carregue mais).

Seu servidor deve ser responsável por retornar novos registros (carregar mais registros ou novos registros por meio de puxar para atualizar), bem como IDs de registros excluídos de ["id1", "id2", "id3", "id4", "id5", " id6 "," id7 "," id8 "," id9 "," id10 "].

Exemplo: - Se você está solicitando carregar mais, sua solicitação deve ser algo como isto: -

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

Agora, suponha que você esteja solicitando registros antigos (carregue mais) e suponha que o registro "id2" seja atualizado por alguém e os registros "id5" e "id8" sejam excluídos do servidor, e a resposta do servidor deverá ser algo como:

{
        "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"]
}

Mas, neste caso, se você tiver muitos registros em cache locais, suponha 500, sua sequência de solicitação será muito longa assim: -

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

Abordagem 2: Quando o servidor é inteligente o suficiente para lidar com os estados dos objetos de acordo com a data.

Você pode enviar o ID do primeiro registro e o último registro e o tempo da época da solicitação anterior. Dessa forma, sua solicitação é sempre pequena, mesmo se você tiver uma grande quantidade de registros em cache

Exemplo: - Se você está solicitando carregar mais, sua solicitação deve ser algo como isto: -

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

Seu servidor é responsável por retornar os IDs dos registros excluídos que são excluídos após o last_request_time, bem como retornar o registro atualizado após last_request_time entre "id1" e "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"]
}

Puxe para atualizar: -

insira a descrição da imagem aqui

Carregue mais

insira a descrição da imagem aqui

Mohd Iftekhar Qurashi
fonte
14

Pode ser difícil encontrar práticas recomendadas, já que a maioria dos sistemas com APIs não se adapta a esse cenário, porque é uma vantagem extrema ou eles geralmente não excluem registros (Facebook, Twitter). O Facebook realmente diz que cada "página" pode não ter o número de resultados solicitados devido à filtragem feita após a paginação. https://developers.facebook.com/blog/post/478/

Se você realmente precisar acomodar esse gabinete de borda, precisará "lembrar" de onde parou. A sugestão de jandjorgensen é quase imediata, mas eu usaria um campo garantido como único como a chave primária. Pode ser necessário usar mais de um campo.

Seguindo o fluxo do Facebook, você pode (e deve) armazenar em cache as páginas já solicitadas e apenas retornar aquelas com linhas excluídas filtradas se solicitarem uma página que já haviam solicitado.

Brent Baisley
fonte
2
Esta não é uma solução aceitável. Consome bastante tempo e memória. Todos os dados excluídos, juntamente com os dados solicitados, precisarão ser mantidos na memória, que pode não ser usada se o mesmo usuário não solicitar mais entradas.
perfil completo de Deepak Garg
3
Discordo. Apenas manter os IDs exclusivos não usa muita memória. Você não retém os dados indefinidamente, apenas para a "sessão". Isso é fácil com o memcache, basta definir a duração do vencimento (ou seja, 10 minutos).
Brent Baisley
a memória é mais barata que a velocidade da rede / CPU. Então, se a criação de uma página é muito caro (em termos de rede ou é intensivo da CPU), então o cache de resultados é uma abordagem válida @DeepakGarg
U Avalos
9

A paginação é geralmente uma operação de "usuário" e, para evitar sobrecarga nos computadores e no cérebro humano, você geralmente atribui um subconjunto. No entanto, em vez de pensar que não recebemos a lista inteira, talvez seja melhor perguntar , isso importa?

Se uma visualização precisa de rolagem ao vivo for necessária, as APIs REST que são de natureza de solicitação / resposta não são adequadas para esse propósito. Para isso, considere WebSockets ou Eventos enviados pelo servidor HTML5 para informar seu front-end ao lidar com alterações.

Agora, se houver necessidade obter uma captura instantânea dos dados, eu apenas forneceria uma chamada de API que forneça todos os dados em uma solicitação sem paginação. Lembre-se de que você precisaria de algo que faria o streaming da saída sem carregá-la temporariamente na memória se você tiver um conjunto de dados grande.

Para o meu caso, designo implicitamente algumas chamadas de API para permitir a obtenção de todas as informações (principalmente os dados da tabela de referência). Você também pode proteger essas APIs para que não danifiquem seu sistema.

Archimedes Trajano
fonte
8

Opção A: Paginação do conjunto de chaves com um carimbo de data e hora

Para evitar os inconvenientes da paginação deslocada que você mencionou, você pode usar a paginação baseada em conjunto de chaves. Geralmente, as entidades têm um registro de data e hora que indica seu horário de criação ou modificação. Esse registro de data e hora pode ser usado para paginação: basta passar o registro de data e hora do último elemento como parâmetro de consulta para a próxima solicitação. O servidor, por sua vez, usa o registro de data e hora como critério de filtro (por exemplo 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"
    }
}

Dessa forma, você não perderá nenhum elemento. Essa abordagem deve ser boa o suficiente para muitos casos de uso. No entanto, lembre-se do seguinte:

  • Você pode executar ciclos infinitos quando todos os elementos de uma única página tiverem o mesmo carimbo de data / hora.
  • Você pode entregar muitos elementos várias vezes ao cliente quando elementos com o mesmo carimbo de data / hora estão sobrepondo duas páginas.

Você pode tornar essas desvantagens menos prováveis, aumentando o tamanho da página e usando registros de data e hora com precisão de milissegundos.

Opção B: Paginação estendida do conjunto de chaves com um token de continuação

Para lidar com as desvantagens mencionadas da paginação normal do conjunto de teclas, você pode adicionar um deslocamento ao carimbo de data e hora e usar o chamado "Continuation Token" ou "Cursor". O deslocamento é a posição do elemento em relação ao primeiro elemento com o mesmo registro de data e hora. Geralmente, o token tem um formato parecido Timestamp_Offset. Ele é passado para o cliente na resposta e pode ser enviado de volta ao servidor para recuperar a próxima página.

{
    "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"
    }
}

O token "1512757072_2" aponta para o último elemento da página e declara "o cliente já obteve o segundo elemento com o carimbo de data / hora 1512757072". Dessa forma, o servidor sabe para onde continuar.

Lembre-se de que você deve lidar com casos em que os elementos foram alterados entre duas solicitações. Isso geralmente é feito adicionando uma soma de verificação ao token. Essa soma de verificação é calculada sobre os IDs de todos os elementos com esse registro de data e hora. Então, acabamos com um formato de token como este:Timestamp_Offset_Checksum .

Para obter mais informações sobre essa abordagem, consulte a postagem do blog " Paginação da API da Web com tokens de continuação ". Uma desvantagem dessa abordagem é a implementação complicada, pois há muitos casos extremos que precisam ser levados em consideração. É por isso que bibliotecas como o token de continuação podem ser úteis (se você estiver usando a linguagem Java / a JVM). Isenção de responsabilidade: sou o autor da postagem e co-autor da biblioteca.

phauer
fonte
4

Eu acho que atualmente sua API está realmente respondendo da maneira que deveria. Os 100 primeiros registros da página na ordem geral de objetos que você está mantendo. Sua explicação diz que você está usando algum tipo de ID de pedido para definir a ordem dos seus objetos para paginação.

Agora, se você quiser que a página 2 sempre comece de 101 e termine em 200, faça o número de entradas na página como variável, pois elas estão sujeitas a exclusão.

Você deve fazer algo como o pseudocódigo abaixo:

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)
mickeymoon
fonte
1
Concordo. em vez de consultar por número de registro (o que não é confiável), você deve consultar por ID. Altere sua consulta (x, m) para significar "retornar até m registros SORTED por ID, com ID> x", então você pode simplesmente definir x como o ID máximo do resultado da consulta anterior.
John Henckel
É verdade, quer classificar ids ou se você tem alguma área de negócio concreto para classificar como creation_date etc.
mickeymoon
4

Apenas para adicionar a esta resposta de Kamilk: https://www.stackoverflow.com/a/13905589

Depende muito do tamanho do conjunto de dados em que você está trabalhando. Pequenos conjuntos de dados funcionam efetivamente na paginação offset, mas grandes conjuntos de dados em tempo real exigem paginação do cursor.

Encontrei um artigo maravilhoso sobre como o Slack evoluiu a paginação de sua API, pois os conjuntos de dados aumentavam explicando os pontos positivos e negativos em todas as etapas: https://slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12

Shubham Srivastava
fonte
3

Eu pensei muito sobre isso e finalmente terminei com a solução que descreverei abaixo. É um grande avanço na complexidade, mas se você fizer esse passo, terá o que realmente procura, que são resultados determinísticos para solicitações futuras.

Seu exemplo de um item sendo excluído é apenas a ponta do iceberg. E se você estiver filtrando, color=bluemas alguém alterar as cores dos itens entre as solicitações? Buscar todos os itens de maneira paginável de forma confiável é impossível ... a menos que ... implementemos o histórico de revisões .

Eu o implementei e é realmente menos difícil do que eu esperava. Aqui está o que eu fiz:

  • Eu criei uma única tabela changelogs com uma coluna de ID de incremento automático
  • Minhas entidades têm um id campo, mas essa não é a chave primária
  • As entidades têm um changeIdcampo que é a chave primária e também uma chave estrangeira para os registros de alterações.
  • Sempre que um usuário cria, atualiza ou exclui um registro, o sistema insere um novo registro changelogs, pega o ID e o atribui a uma nova versão da entidade, que depois insere no banco de dados
  • Minhas consultas selecionam o changeId máximo (agrupado por ID) e se associam automaticamente para obter as versões mais recentes de todos os registros.
  • Os filtros são aplicados aos registros mais recentes
  • Um campo de estado controla se um item é excluído
  • O changeId máximo é retornado ao cliente e adicionado como um parâmetro de consulta em solicitações subsequentes
  • Como apenas novas alterações são criadas, cada changeId representa uma captura instantânea exclusiva dos dados subjacentes no momento em que a alteração foi criada.
  • Isso significa que você pode armazenar em cache os resultados de solicitações que tenham o parâmetro changeIdnelas para sempre. Os resultados nunca expiram porque nunca mudam.
  • Isso também abre recursos interessantes, como reversão / reversão, sincronização do cache do cliente etc. Quaisquer recursos que se beneficiem do histórico de alterações.
Stijn de Witt
fonte
estou confuso. Como isso resolve o caso de uso que você mencionou? (Um campo aleatório muda no cache e você quer invalidar o cache)
U Avalos
Para todas as alterações que você fizer, basta olhar para a resposta. O servidor fornecerá um novo changeId e você o utilizará na sua próxima solicitação. Para outras alterações (feitas por outras pessoas), você pesquisa a última alteração de vez em quando e, se ela for maior que a sua, você sabe que há alterações excelentes. Ou você configura algum sistema de notificação (sondagem longa. Envio de servidor, websockets) que alerta o cliente quando há alterações pendentes.
Stijn de Witt
0

Outra opção para Paginação em APIs RESTFul, é usar o cabeçalho Link apresentado aqui . Por exemplo, o Github usa- o da seguinte forma:

Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
  <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

Os valores possíveis para relsão: primeiro, último, próximo, anterior . Mas, usando o Linkcabeçalho, pode não ser possível especificar total_count (número total de elementos).

adnanmuttaleb
fonte