Paginação em uma coleção restante

134

Estou interessado em expor uma interface REST direta a coleções de documentos JSON (pense em CouchDB ou Persevere ). O problema que eu estou enfrentando é como lidar com a GEToperação na raiz da coleção, se a coleção for grande.

Como exemplo, finja que estou expondo a Questionstabela do StackOverflow em que cada linha é exposta como um documento (não que exista necessariamente uma tabela desse tipo, apenas um exemplo concreto de uma coleção considerável de 'documentos'). A coleção seria disponibilizado na /db/questionscom a api CRUD de costume GET /db/questions/XXX, PUT /db/questions/XXX, POST /db/questionsestá em jogo. A maneira padrão de obter toda a coleção é, GET /db/questionsmas se isso ingenuamente despejar cada linha como um objeto JSON, você terá um download bastante considerável e muito trabalho por parte do servidor.

A solução é, obviamente, a paginação. O Dojo resolveu esse problema em seu JsonRestStore por meio de uma inteligente extensão compatível com RFC2616, usando o Rangecabeçalho com uma unidade de intervalo personalizada items. O resultado é um 206 Partial Contentque retorna apenas o intervalo solicitado. A vantagem dessa abordagem sobre um parâmetro de consulta é que ele deixa a string de consulta para ... consultas (por exemplo, GET /db/questions/?score>200ou algo assim, e sim, isso seria codificado %3E).

Essa abordagem cobre completamente o comportamento que eu quero. O problema é que o RFC 2616 especifica que, em uma resposta 206 (ênfase minha):

O pedido DEVE ter incluído um campo de cabeçalho Range ( seção 14.35 ) indicando o intervalo desejado, e PODE ter incluído um campo de cabeçalho If-Range ( seção 14.27 ) para tornar o pedido condicional.

Isso faz sentido no contexto do uso padrão do cabeçalho, mas é um problema, porque eu gostaria que a resposta 206 fosse o padrão para lidar com clientes ingênuos / pessoas aleatórias explorando.

Analisei detalhadamente a RFC em busca de uma solução, mas fiquei insatisfeito com minhas soluções e estou interessado na opinião do SO sobre o problema.

Idéias que tive:

  • Retorne 200com um Content-Rangecabeçalho! - Não acho que isso esteja errado, mas eu prefiro um indicador mais óbvio de que a resposta é apenas Conteúdo Parcial.
  • Retorno400 Range Required - Não há um código de resposta especial 400 para os cabeçalhos necessários; portanto, o erro padrão deve ser usado e lido manualmente. Isso também dificulta a exploração via navegador da Web (ou outro cliente como o Resty).
  • Use um parâmetro de consulta - A abordagem padrão, mas espero permitir consultas a la Persevere e isso corta o espaço para nome da consulta.
  • Basta voltar 206! - Eu acho que a maioria dos clientes não iria surtar, mas eu prefiro não ir contra um DEVE na RFC
  • Estenda a especificação! Retorno266 Partial Content - se comporta exatamente como o 206, mas é em resposta a uma solicitação que NÃO DEVE conter o Rangecabeçalho. Eu acho que o 266 é alto o suficiente para não ter problemas com colisões e isso faz sentido para mim, mas não tenho certeza se isso é considerado tabu ou não.

Eu acho que esse é um problema bastante comum e gostaria de ver isso feito de uma maneira de fato, para que eu ou outra pessoa não esteja reinventando a roda.

Qual é a melhor maneira de expor uma coleção completa via HTTP quando a coleção é grande?

Karl Guertin
fonte
21
Uau, esse é um bom exemplo de pergunta em que algum pensamento sério já foi feito antes.
Heiko Rupp
possível duplicata de paginação em um aplicativo REST web
rds
1
Quanto à abordagem do Dojo em usar o cabeçalho Range, embora o Accept-Ranges permita a extensão, pelo que sei, o EBNF for Range não: tools.ietf.org/html/rfc2616#section-14.35.2 . A especificação indica Range = "Range" ":" ranges-specifieronde o último em tools.ietf.org/html/rfc2616#section-14.35.1 é descrito meramente como "byte-range-specifier", que deve começar com "bytes-unit", que é definido como a string "bytes "
Brett Zamir
2
O Content-Rangecabeçalho se aplica ao corpo (pode ser usado com solicitação ao fazer upload de arquivos grandes etc, ou como resposta ao fazer o download). O Rangecabeçalho é usado para solicitar um determinado intervalo. Deve-se responder 206quando o Rangecabeçalho foi incluído na solicitação. Caso contrário, a resposta ainda pode incluir um Content-Rangecabeçalho, mas o código de resposta deve estar 200. Na verdade, esse cabeçalho parece ideal para paginação.
Stijn de Witt
Mas o próprio RFC 2616 diz que "as implementações HTTP / 1.1 PODEM ignorar os intervalos especificados usando outras unidades". Portanto, é uma boa prática usar cabeçalhos de intervalo para paginação? porque pode comprometer a interoperabilidade.
choulwar chetan

Respostas:

23

Minha intuição é que as extensões de intervalo HTTP não foram projetadas para o seu caso de uso e, portanto, você não deve tentar. Uma resposta parcial implica 206e 206só deve ser enviada se o cliente solicitou.

Você pode considerar uma abordagem diferente, como a usada no Atom (em que a representação por design pode ser parcial e é retornada com um status 200e, potencialmente, com links de paginação). Veja RFC 4287 e RFC 5005 .

Julian Reschke
fonte
14
O uso do Dojo está completamente dentro das especificações. Se o servidor não entender a itemsunidade de alcance, ele retornará uma resposta completa. Estou familiarizado com o Atom, mas essa não é a solução geral para a paginação Rest. Esta não é uma solução para um único caso, mais do que deveria ser a solução geral. Nem todos os documentos / coleções se encaixam no modelo Atom e não há motivo para forçá-lo, a menos que seja necessário.
Karl Guertin
1
@KarlGuertin concordou. Pena que esta é a resposta aceita, porque parece que muitos na comunidade estão realmente adotando Rangee Content-Rangepara fins de paginação.
Stijn de Witt
34

Eu realmente não concordo com alguns de vocês. Estou trabalhando há semanas nesses recursos para o meu serviço REST. O que acabei fazendo é realmente simples. Minha solução só faz sentido para o que as pessoas REST chamam de coleção.

O cliente DEVE incluir um cabeçalho "Range" para indicar de que parte da coleção ele precisa ou estar preparado para lidar com um erro quando a coleção solicitada é muito grande para ser recuperada em uma única viagem de ida e volta.

O servidor envia uma resposta, com o cabeçalho Content-Range especificando qual parte do recurso foi enviada e um cabeçalho ETag para identificar a versão atual da coleção. Normalmente, uso um ETag semelhante ao Facebook {last_modification_timestamp} - {resource_id} e considero que o ETag de uma coleção é o recurso modificado mais recente que ele contém.

Para solicitar uma parte específica de uma coleção, o cliente DEVE usar o cabeçalho "Range" e preencher o cabeçalho "If-Match" com o ETag da coleção obtida de solicitações realizadas anteriormente para adquirir outras partes da mesma coleção. O servidor pode, portanto, verificar se a coleção não foi alterada antes de enviar a parte solicitada. Se existir uma versão mais recente, uma resposta 412 PRECONDITION FAILED será retornada para convidar o cliente a recuperar a coleção do zero. Isso é necessário porque pode significar que alguns recursos podem ter sido adicionados ou removidos antes ou depois da parte solicitada no momento.

Eu uso ETag / If-Match em conjunto com Last-Modified / If-Unmodified-Since para otimizar o cache. Navegadores e proxies podem contar com um ou ambos para seus algoritmos de armazenamento em cache.

Eu acho que um URL deve estar limpo, a menos que inclua uma consulta de pesquisa / filtro. Se você pensar bem, uma pesquisa nada mais é do que uma visão parcial de uma coleção. Em vez dos carros / pesquisa? Q = URLs do tipo BMW, devemos ver mais carros? Fabricante = BMW.

Mohamed
fonte
Você quis dizer 416 "Intervalo solicitado não satisfatório" ou "413" Entidade de solicitação muito grande?
1
@ Mohamed, eu acho que você quer dizer If-Unmodified-Since, o que corresponde à variante E-Tag If-Match, em vez de If-Modified-Since. Dito isto, você também pode considerar remover essa restrição, dependendo do seu caso de uso. Digamos que você tenha uma coleção que cresça apenas a partir do topo (como uma coleção de estilos "primeiro mais novo"), o pior que pode acontecer se essa coleção mudar entre solicitações é que um usuário que percorre uma coleção vê as entradas duas vezes. (Que em si também é uma informação útil: Ele informa ao usuário a coleção mudou)
Eugene Beresovsky
20
413 é "Entidade solicitada muito grande", não "Entidade solicitada muito grande". Isso significa que o tamanho da sua solicitação, por exemplo, ao fazer upload de um arquivo, é maior do que o servidor deseja processar. Portanto, usá-lo para isso não parece ser completamente apropriado.
User247702
@ Mohammed Eu sei que é uma pergunta antiga, mas se o ETag de uma coleção é o ETag do recurso modificado mais recentemente que a coleção contém, que valor do cabeçalho If-Match deve ser usado ao modificar um recurso na coleção? O uso do valor do ETag retornado com a coleção está errado, pois o cliente poderá modificar o recurso, mesmo que ele não veja o último estado do recurso.
Mickael Marrache
8
Discordo totalmente sobre o uso 413. Este é um código de erro que significa que o cliente está enviando algo que o servidor se recusa a aceitar devido ao tamanho. Não o contrário! Consulte tools.ietf.org/html/rfc7231#section-6.5.11 (observe que diz carga útil de solicitação . Não carga útil de resposta )!
Exhuma
7

Você ainda pode retornar Accept-Rangese Content-Rangescom um 200código de resposta. Esses dois cabeçalhos de resposta fornecem informações suficientes para inferir as mesmas informações que um 206código de resposta fornece explicitamente.

Eu usaria Rangepara paginação e simplesmente retornaria a 200para uma planície GET.

Parece 100% RESTful e não torna a navegação mais difícil.

Edit: eu escrevi um post sobre isso: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

John Gietzen
fonte
5

Se houver mais de uma página de respostas e você não quiser oferecer a coleção inteira de uma só vez, isso significa que existem várias opções?

Em uma solicitação para /db/questions, retorne 300 Multiple Choicescom Linkcabeçalhos que especificam como chegar a cada página, bem como um objeto JSON ou página HTML com uma lista de URLs.

Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...

Você teria um Linkcabeçalho para cada página de resultados (uma sequência vazia significa o URL atual, e o URL é o mesmo para cada página, apenas acessado com intervalos diferentes), e o relacionamento é definido como personalizado de acordo com as próximas Linkespecificações . Esse relacionamento explicaria seu costume 266ou sua violação 206. Esses cabeçalhos são sua versão legível por máquina, já que todos os seus exemplos exigem um cliente de entendimento de qualquer maneira.

(Se você 2xxseguir a rota "range", acredito que seu próprio código de retorno, como você o descreveu, seria o melhor comportamento aqui. Espera-se que você faça isso para seus aplicativos e esses códigos de status HTTP são extensíveis. "] e você tem boas razões.)

300 Multiple Choicesdiz que você também deve fornecer um corpo com uma maneira de escolher o agente do usuário. Se seu cliente está entendendo, ele deve usar os Linkcabeçalhos. Se for um usuário navegando manualmente, talvez uma página HTML com links para um recurso raiz "paginado" especial que possa processar a renderização dessa página específica com base no URL? /humanpage/1/db/questionsou algo horrível assim?


Os comentários no post de Richard Levasseur me lembram uma opção adicional: o Acceptcabeçalho (seção 14.1). Quando a especificação oEmbed saiu, perguntei-me por que não havia sido feito inteiramente usando HTTP e escrevi uma alternativa usando-as.

Mantenha as 300 Multiple Choices, os Linkcabeçalhos e a página HTML para um HTTP ingênua inicial GET, mas ao invés de faixas de uso, têm o seu novo relacionamento paginação definir o uso do Acceptcabeçalho. Sua solicitação HTTP subsequente pode se parecer com isso:

GET /db/questions HTTP/1.1
Host: paged.collection.example
Accept: application/json;PagingSpec=1.0;page=1

O Acceptcabeçalho permite definir um tipo de conteúdo aceitável (seu retorno JSON), além de parâmetros extensíveis para esse tipo (seu número de página). Analisando minhas anotações com base na minha redação escrita oEmbed (não é possível vinculá-la aqui, vou listá-la no meu perfil), você pode ser muito explícito e fornecer uma versão de especificação / relação aqui, caso precise redefinir o pagesignificado do parâmetro no futuro.

Vitorio
fonte
1
+1 nos cabeçalhos dos links, mas eu também recomendaria o primeiro, o anterior, o próximo, o último rels comuns, bem como o arquivo anterior, o próximo arquivo e o atual do RFC5005.
31119 Joseph Holsten
> Em uma solicitação para / db / questions, retorne 300 Várias opções com cabeçalhos de link que especificam como chegar a cada página [..] O problema com isso (e com os designs REST mais puros) é que ele está matando por latência. O objetivo é minimizar as solicitações de rede. Essa primeira solicitação deve gerar resultados, não links para mais solicitações que eventualmente fornecerão os dados de que precisamos.
Stijn de Witt
4

Editar:

Depois de pensar um pouco mais, estou inclinado a concordar que os cabeçalhos de intervalo não são apropriados para paginação. A lógica é que o cabeçalho Range é destinado à resposta do servidor, não aos aplicativos. Se você forneceu 100 megabytes de resultados, mas o servidor (ou cliente) pudesse processar apenas 1 megabyte por vez, bem, é para isso que serve o cabeçalho Range.

Também sou da opinião de que um subconjunto de recursos é seu próprio recurso (semelhante à álgebra relacional.), Por isso merece representação na URL.

Então, basicamente, eu retrocedo minha resposta original (abaixo) sobre o uso de um cabeçalho.


Acho que você respondeu à sua própria pergunta, mais ou menos - retorne 200 ou 206 com o intervalo de conteúdo e, opcionalmente, use um parâmetro de consulta. Eu farejava o agente do usuário e o tipo de conteúdo e, dependendo deles, procurava um parâmetro de consulta. Caso contrário, exija os cabeçalhos do intervalo.

Você tem essencialmente objetivos conflitantes - permita que as pessoas usem o navegador para explorar (o que não permite facilmente cabeçalhos personalizados) ou force as pessoas a usar um cliente especial que pode definir cabeçalhos (o que não permite que eles explorem).

Você pode simplesmente fornecer a eles um cliente especial, dependendo da solicitação - se parecer um navegador simples, envie um pequeno aplicativo ajax que renderize a página e defina os cabeçalhos necessários.

Obviamente, há também o debate sobre se a URL deve conter todo o estado necessário para esse tipo de coisa. A especificação do intervalo usando cabeçalhos pode ser considerada "inanimada" por alguns.

Como um aparte, seria bom se os servidores respondessem com um cabeçalho "Can-Specify: Header1, header2" e os navegadores da Web apresentassem uma interface do usuário para que os usuários pudessem preencher os valores, se desejassem.

Richard Levasseur
fonte
Obrigado pela resposta. Pensei no assunto, mas esperava receber uma segunda opinião. Aconteceu ter um ponteiro para os argumentos do cabeçalho?
Karl Guertin
Aqui está o único que eu marquei (veja a discussão nos comentários): barelyenough.org/blog/2008/05/versioning-rest-web-services Outro site girou em torno do uso de Ruby de .json, .xml,. o tipo de conteúdo de uma solicitação. Alguns dos exemplos: * idioma - colocá-lo no URL significa que o envio do link para outro país o tornaria no idioma errado. * Pagination - colocá-lo no meio de cabeçalho não pode ligar as pessoas para o que você vê
Richard Levasseur
* tipo de conteúdo: uma combinação de problemas de idioma e paginação - se estiver no URL, e se o cliente não suportar esse tipo de conteúdo (por exemplo, uma extensão .ajax e .html)? Por outro lado, sem esse tipo de conteúdo no URL, você não pode garantir a mesma representação. "novo site do ajax! example.com/cool.ajax" vs "artigo interessante aqui: example.com/article.ajax#id=123".
Richard Levasseur
2
IMO, se vai ou não no URL depende do que é. Minha regra geral é que, se ele identificar um recurso concreto (seja um recurso em um estado específico, seleção de recursos ou resultado discreto), ele será inserido no URL. Consultas de pesquisa, paginação e transações repousantes são bons exemplos disso. Se for algo necessário para transformar a representação abstrata em uma representação concreta, ela será exibida no cabeçalho. informações de autenticação e tipo de conteúdo são bons exemplos disso.
Richard Levasseur
Penso na string de consulta em uma URL como opções para consultar o recurso que está sendo especificado.
wprl
3

Você pode considerar usar um modelo parecido com o Atom Feed Protocol, pois ele tem um modelo de coleções HTTP sensato e como manipulá-las (onde insana significa WebDAV).

Existe o Atom Publishing Protocol, que define o modelo de coleção e as operações REST, além de você poder usar o RFC 5005 - Feed Paging and Archiving para percorrer grandes coleções.

A mudança do conteúdo do Atom XML para JSON não deve afetar a ideia.

dajobe
fonte
3

Acho que o verdadeiro problema aqui é que não há nada na especificação que nos diga como fazer redirecionamentos automáticos quando confrontados com 413 - Entidade solicitada muito grande.

Eu estava lutando com esse mesmo problema recentemente e procurei inspiração no livro RESTful Web Services . Pessoalmente, não acho que o 206 seja apropriado devido ao requisito do cabeçalho. Meus pensamentos também me levaram a 300, mas achei que isso era mais para diferentes tipos de mímica, então procurei o que Richardson e Ruby tinham a dizer sobre o assunto no Apêndice B, página 377. Eles sugerem que o servidor apenas escolha o preferido representação e enviá-lo de volta com um 200, basicamente ignorando a noção de que deveria ser um 300.

Isso também combina com a noção de links para os próximos recursos que temos do átomo. A solução que implementei foi adicionar chaves "next" e "previous" ao mapa json que eu estava enviando de volta e pronto.

Posteriormente, comecei a pensar que talvez a coisa a fazer é enviar um 307 - Redirecionamento temporário para um link que seria algo como / db / questions / 1,25 - que deixa o URI original como o nome do recurso canônico, mas é necessário um recurso subordinado nomeado adequadamente. Esse é um comportamento que eu gostaria de ver em um 413, mas o 307 parece um bom compromisso. Ainda não tentei isso no código ainda. O que seria ainda melhor é o redirecionamento redirecionar para um URL que contém os IDs reais das perguntas mais recentes. Por exemplo, se cada pergunta tiver um ID inteiro, e houver 100 perguntas no sistema e você desejar mostrar as dez mais recentes, as solicitações para / db / questions deverão ser 307 direcionadas para / db / questions / 100,91

Esta é uma pergunta muito boa, obrigado por perguntar. Você confirmou para mim que eu não sou louco por ter passado dias pensando nisso.

stinkymatt
fonte
303 seria melhor nesse sentido do que 307. 307 implica que o URL original em breve começará a responder conforme o cliente espera.
Nicholas Shanks
O RFC 7231 faz referência ao código de status HTTP 413 como Payload Too Large e relaciona esse código com o tamanho da solicitação e não com o tamanho potencial da resposta.
22418 Beawolf
1

Você pode detectar o Rangecabeçalho e imitar o Dojo, se estiver presente, e imitar o Atom, se não estiver. Parece-me que isso divide claramente os casos de uso. Se você estiver respondendo a uma consulta REST do seu aplicativo, espera que ele seja formatado com um Rangecabeçalho. Se você estiver respondendo a um navegador casual, se retornar os links de paginação, a ferramenta fornecerá uma maneira fácil de explorar a coleção.

Greg
fonte
1

Um dos grandes problemas com os cabeçalhos de intervalo é que muitos proxies corporativos os filtram. Eu aconselho a usar um parâmetro de consulta.

user64141
fonte
0

Parece-me que a melhor maneira de fazer isso é incluir o intervalo como parâmetros de consulta. por exemplo, GET / db / questions /? date> mindate e date <maxdate . Em um GET para o / db / questions / sem parâmetros de consulta, retorne 303 com Local: / db / questions /? Query-parameters-to-get-the-default-page . Em seguida, forneça um URL diferente pelo qual quem está consumindo sua API para obter estatísticas sobre a coleção (por exemplo, quais parâmetros de consulta usar se quiser a coleção inteira);

Dathan
fonte
0

Embora seja possível usar o cabeçalho Range para esse fim, não acho que tenha sido essa a intenção. Parece ter sido projetado para lidar com conexões inadequadas e também para limitar os dados (para que o cliente possa solicitar parte da solicitação se algo estiver faltando ou se o tamanho for muito grande para processar). Você está invadindo a paginação em algo que provavelmente é usado para outros fins na camada de comunicação. A maneira "adequada" de lidar com a paginação é com os tipos que você retorna. Em vez de retornar o objeto de perguntas, você deve retornar um novo tipo.

Portanto, se as perguntas forem assim:

<questions> <question index=1></question> <question index=2></question> ... </questions>

O novo tipo pode ser algo como isto:

<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>

É claro que você controla seus tipos de mídia, para que você possa transformar suas "páginas" em um formato adequado às suas necessidades. Se você criar algo genérico, poderá ter um único analisador no cliente para manipular a mesma paginação para todos os tipos. Eu acho que isso está mais no espírito da especificação HTTP, em vez de falsificar o parâmetro Range para outra coisa.

Jeremyh
fonte