Paginação em um aplicativo Web REST

329

Esta é uma reformulação mais genérica dessa questão (com a eliminação das partes específicas do Rails)

Não sei como implementar a paginação em um recurso em um aplicativo Web RESTful. Supondo que eu tenha um recurso chamado products, qual das seguintes opções você acha que é a melhor abordagem e por quê:

1. Usando apenas cadeias de consulta

por exemplo. http://application/products?page=2&sort_by=date&sort_how=asc
O problema aqui é que não consigo usar o cache de página inteira e também o URL não é muito limpo e fácil de lembrar.

2. Usando páginas como recursos e cadeias de consulta para classificação

por exemplo. http://application/products/page/2?sort_by=date&sort_how=asc
Nesse caso, o problema que se vê é que http://application/products/pages/1não é um recurso exclusivo, pois o uso sort_by=pricepode gerar um resultado totalmente diferente e ainda não consigo usar o cache da página.

3. Usando páginas como recursos e um segmento de URL para classificação

por exemplo. http://application/products/by-date/page/2
Pessoalmente, não vejo problema em usar esse método, mas alguém me avisou que esse não é um bom caminho a seguir (ele não deu um motivo, por isso, se você souber por que não é recomendado, entre em contato)

Quaisquer sugestões, opiniões e críticas são bem-vindas. Obrigado.

e eu
fonte
34
Esta é uma grande pergunta.
Iain Holder
7
Pergunta de bônus: como as pessoas geralmente especificam os tamanhos das páginas?
Heiko Rupp
Não se esqueça dos parâmetros da matriz w3.org/DesignIssues/MatrixURIs.html
CMCDragonkai

Respostas:

66

Eu acho que o problema com a versão 3 é mais um problema do "ponto de vista" - você vê a página como recurso ou produtos na página.

Se você vir a página como o recurso, é uma solução perfeita, pois a consulta da página 2 sempre produzirá a página 2.

Mas se você vê os produtos na página como o recurso, tem o problema de os produtos na página 2 poderem ser alterados (produtos antigos excluídos ou o que for), nesse caso, o URI nem sempre retorna os mesmos recursos.

Por exemplo, um cliente armazena um link para a página da lista de produtos X. Na próxima vez em que o link for aberto, o produto em questão poderá não estar mais na página X.

Fionn
fonte
6
Bem, mas se você excluir algo, não deve haver mais nada no mesmo URI. Se você excluir todos os produtos da página X - a página X ainda poderá ser válida, mas agora conterá os produtos da página X + 1. Portanto, o URI da página X se tornará o URI da página X + 1 se você o visualizar em "exibição de recursos do produto" "
Fionn
1
> Se você vê a página como o recurso, é uma solução perfeitamente adequada, pois a consulta da página 2 sempre produzirá a página 2. Faz algum sentido? O mesmo URL (qualquer URL que mencione a página 2) sempre produzirá a página 2, independentemente do seu recurso.
temoto
2
Ver a página como recurso provavelmente deve apresentar o POST / foo / page para criar uma nova página, certo?
temoto 04/12/2009
18
Sua resposta vai suavemente para "a solução correta é 1", mas não indica.
Temoto #
2
Na minha opinião, página é um conceito flutuante e não está relacionado ao domínio subjacente. E, portanto, não deve ser considerado como um recurso. Quero dizer flutuando no sentido de que é fluido, que o conceito de página muda com o contexto; um usuário da sua API pode ser um aplicativo móvel, que pode consumir apenas 2 produtos por página, enquanto o outro é um aplicativo de máquina que pode consumir toda a lista. Em resumo, a página é uma "representação" da entidade do domínio subjacente (produto) e não deve ser incluída como parte da URL; somente como um parâmetro de consulta.
Kingz
106

Eu concordo com Fionn, mas vou dar um passo adiante e dizer que para mim a página não é um recurso, é uma propriedade da solicitação. Isso me faz escolher apenas a opção 1 da string de consulta. Parece certo. Eu realmente gosto de como a API do Twitter é estruturada de maneira tranqüila. Não é muito simples, nem muito complicado, bem documentado. Para o bem ou para o mal, é o meu design "ir para" quando estou em cima do muro fazendo algo de um jeito contra o outro.

slf
fonte
28
+1: cadeias de consulta não são identificadores de recursos de primeira classe; eles apenas esclarecem a ordem e o agrupamento do recurso.
precisa saber é o seguinte
1
@ S.Lott A solicitação é o recurso. O que você chama de "recursos de primeira classe" são definidos como valores por Fielding na seção 5.2.1.1 de sua dissertação . Além disso, na mesma seção, Fielding fornece a revisão mais recente de um arquivo de código-fonte como um exemplo de recurso. Como isso pode ser um recurso, mas os 10 últimos produtos podem ser "propriedades da solicitação no recurso de produtos"? Entendo que sua visão é mais prática, mas acho que é menos RESTful.
Edsioufi 5/09/2013
Observe que meu comentário não significa que eu discordo da opção de usar cadeias de consulta em URLs: ambas são soluções viáveis, desde que a API seja orientada por hipermídia, como @RichApodaca mencionou em sua resposta. Estou apenas apontando que a página deve ser considerada como um recurso do ponto de vista do REST.
Edsioufi 5/09/2013
37

O HTTP possui um ótimo cabeçalho Range, que também é adequado para paginação. Você pode enviar

Range: pages=1

ter apenas a primeira página. Isso pode forçá-lo a repensar o que é uma página. Talvez o cliente queira uma gama diferente de itens. O cabeçalho do intervalo também funciona para declarar um pedido:

Range: products-by-date=2009_03_27-

para obter todos os produtos mais novos que essa data ou

Range: products-by-date=0-2009_11_30

para obter todos os produtos mais antigos que essa data. '0' provavelmente não é a melhor solução, mas a RFC parece querer algo para o início do intervalo. Pode haver analisadores HTTP implantados que não analisariam unidades = -range_end.

Se os cabeçalhos não forem uma opção (aceitável), considero que a primeira solução (tudo na string de consulta) é uma maneira de lidar com as páginas. Mas, por favor, normalize as cadeias de consulta (classifique (chave = valor) pares em ordem alfabética). Isso resolve o problema de diferenciação "? A = 1 & b = x" e "? B = x & a = 1".

temoto
fonte
34
Os cabeçalhos podem parecer agradáveis ​​à primeira vista, mas não permitem o compartilhamento da página (por exemplo, copiando o URL). Portanto, para solicitação do ajax, eles podem ser uma boa solução (já que as páginas modificadas pelo ajax não podem ser compartilhadas no estado atual de qualquer maneira), mas eu não as usaria para paginação regular.
Markus
3
E o cabeçalho Range é apenas para intervalos de bytes. Consulte [a especificação dos cabeçalhos HTTP] ( w3.org/Protocols/rfc2616/rfc2616-sec14.html ), seção 14.35.
22712 Chris Westin
16
@ChrisWestin w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 O HTTP / 1.1 usa unidades de intervalo nos campos de cabeçalho Range (seção 14.35) e Content-Range (seção 14.16). range-unit = bytes-unit | other-range-unit Talvez você esteja se referindo a The only range unit defined by HTTP/1.1 is "bytes". HTTP/1.1 implementations MAY ignore ranges specified using other units.Isso não é o mesmo que sua declaração.
temoto
1
@Markus Eu não posso imaginar o caso de uso quando você está compartilhando recursos resto api :)
JakubKnejzlik
O compartilhamento @JakubKnejzlik não é um problema, mas o uso de cabeçalhos HTTP para paginação impede o uso de links HATEOAS para paginação.
Xarx
25

A opção 1 parece a melhor, na medida em que seu aplicativo vê a paginação como uma técnica para produzir uma exibição diferente do mesmo recurso.

Dito isto, o esquema de URL é relativamente insignificante. Se você estiver projetando seu aplicativo para ser direcionado por hipertexto (como todos os aplicativos REST devem ser por definição), seu cliente não estará construindo nenhum URI sozinho. Em vez disso, seu aplicativo fornecerá os links para o cliente e o cliente os seguirá.

Um tipo de link que seu cliente pode fornecer é um link de paginação.

O efeito colateral agradável de tudo isso é que, mesmo que você mude de idéia sobre a estrutura de URI de paginação e implemente algo totalmente diferente na próxima semana, seus clientes poderão continuar trabalhando sem nenhuma modificação.

Rich Apodaca
fonte
3
Bom lembrete sobre o uso de hipermídia como links nos serviços web REST.
Paul D. Eden
11

Eu sempre usei o estilo da opção 1. O cache não tem sido uma preocupação, pois os dados mudam frequentemente de qualquer maneira no meu caso. Se você permitir que o tamanho da página seja configurável, os dados não poderão ser armazenados em cache novamente.

Não acho o URL difícil de lembrar ou imundo. Para mim, este é um bom uso de parâmetros de consulta. O recurso é claramente uma lista de produtos e os parâmetros de consulta estão apenas dizendo como você deseja que a lista seja exibida - classificada e qual página.

John Snyders
fonte
1
+1 Acho que você está certo e eu vou com os parâmetros de consulta (opção 1)
andi
"Não acho difícil lembrar o URL". Essa observação é inútil nos aplicativos REST, pois esses normalmente devem ter apenas um marcador. Se um usuário (ou um aplicativo cliente) tentar "lembrar" o URL, isso é um bom sinal de que a API não é tranqüila.
Edsioufi 5/09/2013
8

Estranho que ninguém tenha apontado que a opção 3 possui parâmetros em uma ordem específica. http // aplicativo / produtos / Data / Decrescente / Nome / Crescente / página / 2 e http // aplicativo / produtos / Nome / Crescente / Data / Decrescente / página / 2

estão apontando para o mesmo recurso, mas têm URLs completamente diferentes.

Para mim, a opção 1 parece a mais aceitável, uma vez que separa claramente "o que eu quero" e "como eu quero" (ele ainda tem um ponto de interrogação entre eles, risos). O cache de página inteira pode ser implementado usando URL completo (todas as opções sofrerão o mesmo problema).

Com a abordagem Parameters-in-URL, o único benefício é o URL limpo. Embora você precise criar uma maneira de codificar parâmetros e decodificá-los sem perdas. Claro que você pode usar o URLencode / decodificar, mas isso tornará os URLs feios novamente :)

TEHEK
fonte
1
Essas são duas ordens diferentes. O primeiro classifica por data decrescente, e apenas quebra laços por nome crescente; o segundo classifica por nome ascendente e apenas quebra vínculos por data decrescente.
Imran Rashid
De fato, os dois URLs de exemplo fornecidos aqui não são apenas diferentes por escrito, mas também por significado. Como denota um caminho, não há garantia de que você encontre a mesma coisa ao virar à esquerda primeiro e à direita depois ou vice-versa. Dito isto, os parâmetros de classificação como partes do caminho da URL têm vantagens formais sobre os parâmetros da URL, que devem ser comutados comutativamente sem alterar o significado geral, mas sofrem de traps de codificação, como é dito aqui.
Christian Gosch
7

Eu preferiria usar parâmetros de consulta offset e limit.

offset : para o índice do item na coleção.

limite : para contagem de itens.

O cliente pode simplesmente continuar atualizando o deslocamento da seguinte maneira

offset = offset + limit

para a próxima página.

O caminho é considerado o identificador de recurso. E uma página não é um recurso, mas um subconjunto da coleção de recursos. Como a paginação geralmente é uma solicitação GET, os parâmetros de consulta são mais adequados para paginação do que para cabeçalhos.

Referência: https://metamug.com/article/rest-api-developers-dilemma.html#Requesting-the-next-page

Classificador
fonte
5

Procurando as melhores práticas, deparei-me com este site:

http://www.restapitutorial.com

Na página de recursos, há um link para baixar um arquivo .pdf que contém as melhores práticas completas de REST sugeridas pelo autor. Em que, entre outras coisas, há uma seção sobre paginação.

O autor sugere adicionar suporte a ambos, usando um cabeçalho Range e usando parâmetros de string de consulta.

Solicitação

Exemplo de cabeçalho HTTP:

Range: items=0-24

Exemplo de parâmetros de cadeia de consulta:

GET http://api.example.com/resources?offset=0&limit=25

Onde deslocamento é o número do item inicial e limite é o número máximo de itens a serem retornados.

Resposta

A resposta deve incluir um cabeçalho Content-Range indicando quantos itens estão sendo retornados e quantos itens totais existem ainda para serem recuperados

Exemplos de cabeçalho HTTP:

Content-Range: items 0-24/66

Content-Range: items 40-65/*

No arquivo .pdf, existem outras sugestões para casos mais específicos.

Mario Arturo
fonte
4

Atualmente, estou usando um esquema semelhante a este em meus aplicativos ASP.NET MVC:

por exemplo http://application/products/by-date/page/2

especificamente é: http://application/products/Date/Ascending/3

No entanto, não estou muito feliz em incluir informações de paginação e classificação na rota dessa maneira.

A lista de itens (produtos neste caso) é mutável. ou seja, na próxima vez que alguém retornar a um URL que inclua parâmetros de paginação e classificação, os resultados obtidos poderão ter sido alterados. Portanto, a idéia de http://application/products/Date/Ascending/3um URL exclusivo que aponta para um conjunto definido e imutável de produtos é perdida.

Steve Willcock
fonte
1
A primeira questão, com a classificação em várias colunas, aplica-se a todos os três métodos na minha opinião. Portanto, não é realmente um favor / contra para nenhum deles. Em relação à segunda questão: isso não aconteceu com nenhum recurso? Um produto, por exemplo, também pode ser editado / excluído.
andi
Eu acho que a classificação em várias colunas é realmente um 'golpe' para todos os três métodos, já que o URL fica maior e mais incontrolável - portanto, uma razão pela qual estou pensando em mudar para formar parâmetros de página / classificação baseados. Para a segunda edição, acho que há uma diferença conceitual fundamental entre um identificador persistente exclusivo, como um ID de produto, e não uma lista transitória de produtos. Para produtos excluídos, uma mensagem, por exemplo, 'Esse produto não existe no sistema', informa algo concreto sobre esse produto.
21420 Steve Willcock
1
Remover todas as informações de paginação e classificação da rota é bom. E colocá-lo nos parâmetros do POST é ruim. Olá? A pergunta é sobre o REST. Não estamos usando o POST apenas para reduzir o URL no REST. Verbo faz sentido.
temoto 04/12/2009
1
Pessoalmente, eu não usaria parâmetros de formulário para uma consulta, porque quase exigiria um método HTTP POST ou PUT (já que agora existe um corpo na solicitação). GET me parece o método mais apropriado a ser usado, pois POST e PUT implicam na modificação do recurso. Devido a isso, eu adicionaria mais parâmetros de consulta à URL quando a classificação por várias colunas for necessária.
Paul D. Eden
1

Costumo concordar com o slf que "página" não é realmente um recurso. Por outro lado, a opção 3 é mais limpa, fácil de ler e pode ser mais facilmente adivinhada pelo usuário e digitada, se necessário. Estou dividido entre as opções 1 e 3, mas não vejo motivo para não usar a opção 3.

Além disso, embora eles tenham uma boa aparência, uma desvantagem do uso de parâmetros ocultos, como alguém mencionado, em vez de cadeias de consulta ou segmentos de URL, é que o usuário não pode marcar ou vincular diretamente a uma página específica. Isso pode ou não ser um problema, dependendo do aplicativo, mas apenas algo para estar ciente.

insane.dreamer
fonte
1
Quanto à sua menção de ser mais fácil de adivinhar, isso não deve importar. Ao criar uma API hipermídia, os usuários nunca devem ter que adivinhar URIs.
JR Garcia
0

Eu usei a solução 3 antes (eu escrevo muitos aplicativos de django). E eu não acho que haja algo errado com isso. É tão gerador quanto os outros dois (caso você precise fazer uma raspagem em massa ou algo semelhante) e parece mais limpo. Além disso, seus usuários podem adivinhar URLs (se for um aplicativo voltado para o público), e as pessoas gostam de poder ir diretamente para onde desejam, e a adivinhação de URLs é poderosa.

Alex
fonte
0

Eu uso em meus projetos os seguintes URLs:

http://application/products?page=2&sort=+field1-field2

o que significa - "dê-me página a segunda página ordenada ascendente pelo campo1 e depois descendente pelo campo2". Ou, se precisar de mais flexibilidade, uso:

http://application/products?skip=20&limit=20&sort=+field1-field2
Eugene
fonte
0

Uso nos seguintes padrões para obter o próximo registro da página. http: // application / products? lastRecordKey =? & pageSize = 20 & sort = ASC

RecordKey é a coluna de uma tabela que contém valor seqüencial no banco de dados. Isso é usado para buscar apenas dados de uma página por vez no DB. pageSize é usado para determinar quantos registros buscar. sort é usado para classificar o registro em ordem crescente ou decrescente.

Susanta Ghosh
fonte