Paging in a Rest Collection


134

Sono interessato a esporre un'interfaccia REST diretta a raccolte di documenti JSON (pensa a CouchDB o Persevere ). Il problema in cui mi imbatto è come gestire l' GEToperazione sulla radice della raccolta se la raccolta è grande.

Ad esempio, fingo di esporre la Questionstabella di StackOverflow in cui ogni riga è esposta come documento (non che esista necessariamente una tabella di questo tipo, solo un esempio concreto di una considerevole raccolta di "documenti"). La collezione sarà disponibile presso /db/questionscon la solita CRUD api GET /db/questions/XXX, PUT /db/questions/XXX, POST /db/questionsè in gioco. Il modo standard per ottenere l'intera raccolta è, GET /db/questionsma se questo ingenua ogni riga come un oggetto JSON, otterrai un download piuttosto considerevole e molto lavoro da parte del server.

La soluzione è, ovviamente, il paging. Dojo ha risolto questo problema nel suo JsonRestStore tramite un'estensione intelligente conforme a RFC2616 dell'utilizzo Rangedell'intestazione con un'unità di intervallo personalizzata items. Il risultato è un valore 206 Partial Contentche restituisce solo l'intervallo richiesto. Il vantaggio di questo approccio rispetto a un parametro di query è che lascia la stringa di query per ... query (ad es. GET /db/questions/?score>200O somesuch, e sì che sarebbe codificato %3E).

Questo approccio copre completamente il comportamento che desidero. Il problema è che RFC 2616 specifica che su una risposta 206 (sottolineatura mia):

La richiesta DEVE aver incluso un campo di intestazione Range ( sezione 14.35 ) che indica l'intervallo desiderato e MAGGIO ha incluso un campo di intestazione If-Range ( sezione 14.27 ) per rendere condizionale la richiesta.

Questo ha senso nel contesto dell'uso standard dell'intestazione, ma è un problema perché vorrei che la risposta 206 fosse l'impostazione predefinita per gestire l'esplorazione di client / persone casuali ingenui.

Ho esaminato dettagliatamente la RFC alla ricerca di una soluzione, ma non sono stato soddisfatto delle mie soluzioni e sono interessato alla soluzione del problema da parte di SO.

Idee che ho avuto:

  • Ritorna 200con un Content-Rangecolpo di testa! - Non penso che sia sbagliato, ma preferirei se fosse un indicatore più ovvio che la risposta è solo contenuto parziale.
  • Restituzione400 Range Required : non esiste uno speciale codice di risposta 400 per le intestazioni richieste, pertanto l'errore predefinito deve essere utilizzato e letto manualmente. Questo rende anche più difficile l'esplorazione tramite browser web (o qualche altro client come Resty).
  • Usa un parametro di query : l'approccio standard, ma spero di consentire le query alla Persevere e questo taglia lo spazio dei nomi delle query.
  • Ritorna 206! - Penso che la maggior parte dei clienti non impazzirebbe, ma preferirei non andare contro un DEVE nella RFC
  • Estendi le specifiche! Return266 Partial Content : si comporta esattamente come 206 ma risponde a una richiesta che NON DEVE contenere l' Rangeintestazione. Immagino che 266 sia abbastanza alto da non dover incorrere in problemi di collisione e ha senso per me, ma non sono chiaro se questo sia considerato tabù o no.

Penso che questo sia un problema abbastanza comune e mi piacerebbe vederlo fatto in una maniera di fatto, quindi io o qualcun altro non stiamo reinventando la ruota.

Qual è il modo migliore per esporre una raccolta completa via HTTP quando la raccolta è grande?


21
Wow, questo è un buon esempio di una domanda in cui un pensiero serio è stato fatto prima.
Heiko Rupp,


1
Per quanto riguarda l'approccio di Dojo nell'uso dell'intestazione Range, sebbene Accept-Ranges consenta l'estensione, da quanto posso dire, l'EBNF per Range non lo fa: tools.ietf.org/html/rfc2616#section-14.35.2 . La specifica indica Range = "Range" ":" ranges-specifierdove quest'ultima in tools.ietf.org/html/rfc2616#section-14.35.1 è descritta semplicemente come "specificatore di intervalli di byte" che deve iniziare con "unità di byte" che è definita come stringa "byte ".
Brett Zamir,

2
L' Content-Rangeintestazione si applica al corpo (può essere utilizzato con la richiesta durante il caricamento di file di grandi dimensioni ecc. O per la risposta durante il download). L' Rangeintestazione viene utilizzata per richiedere un determinato intervallo. Si dovrebbe rispondere 206quando l' Rangeintestazione è stata inclusa nella richiesta. In caso contrario, la risposta potrebbe ancora includere Content-Rangeun'intestazione, ma dovrebbe essere il codice di risposta 200. Questa intestazione in realtà sembra ideale per il paging.
Stijn de Witt,

Ma lo stesso RFC 2616 afferma che "Le implementazioni HTTP / 1.1 POSSONO ignorare gli intervalli specificati usando altre unità." Quindi è una buona pratica usare le intestazioni Range per l'impaginazione? perché potrebbe compromettere l'interoperabilità.
Chetan Choulwar,

Risposte:


23

La mia sensazione è che le estensioni di intervallo HTTP non siano progettate per il tuo caso d'uso, quindi non dovresti provare. Una risposta parziale implica 206e 206deve essere inviata solo se richiesta dal cliente.

È possibile che si desideri prendere in considerazione un approccio diverso, come quello utilizzato in Atom (in cui la rappresentazione in base alla progettazione potrebbe essere parziale e viene restituita con uno stato 200e collegamenti di paginazione potenzialmente). Vedere RFC 4287 e RFC 5005 .


14
L'uso di Dojo è completamente conforme alle specifiche. Se il server non capisce l' itemsunità di portata, restituisce una risposta completa. Conosco Atom, ma questa non è la soluzione generale per il paging di Rest. Questa non è una soluzione per un singolo caso, più di quella che dovrebbe essere la soluzione generale. Non tutti i documenti / raccolte si adattano al modello Atom e non c'è motivo di forzarlo se non richiesto.
Karl Guertin,

1
@KarlGuertin Concordato. Peccato che questa sia la risposta accettata, perché sembra che molti nella comunità stiano effettivamente abbracciando Rangee Content-Rangeper scopi di paginazione.
Stijn de Witt,

34

Non sono davvero d'accordo con alcuni di voi ragazzi. Ho lavorato per settimane su queste funzionalità per il mio servizio REST. Quello che ho finito per fare è davvero semplice. La mia soluzione ha senso solo per ciò che le persone REST chiamano collezione.

Il cliente DEVE includere un'intestazione "Range" per indicare la parte della raccolta di cui ha bisogno, oppure essere pronto a gestire un errore 413 ENTITY RICHIESTO TROPPO GRANDE quando la raccolta richiesta è troppo grande per essere recuperata in un solo round trip.

Il server invia una risposta 206 CONTENUTO PARZIALE, con l'intestazione Content-Range che specifica quale parte della risorsa è stata inviata e un'intestazione ETag per identificare la versione corrente della raccolta. Di solito utilizzo un ETag simile a Facebook {last_modification_timestamp} - {resource_id} e ritengo che l'ETag di una raccolta sia quella della risorsa modificata più di recente che contiene.

Per richiedere una parte specifica di una raccolta, il client DEVE utilizzare l'intestazione "Range" e riempire l'intestazione "If-Match" con l'ETag della raccolta ottenuta da richieste eseguite in precedenza per acquisire altre parti della stessa raccolta. Il server può quindi verificare che la raccolta non sia cambiata prima di inviare la parte richiesta. Se esiste una versione più recente, viene restituita una risposta 412 PRECONDITION FAILED per invitare il client a recuperare la raccolta da zero. Ciò è necessario perché potrebbe significare che alcune risorse potrebbero essere state aggiunte o rimosse prima o dopo la parte attualmente richiesta.

Uso ETag / If-Match insieme a Last-Modified / If-Unmodified-Since per ottimizzare la cache. Browser e proxy potrebbero fare affidamento su uno o entrambi per i loro algoritmi di memorizzazione nella cache.

Penso che un URL dovrebbe essere pulito a meno che non includa una query di ricerca / filtro. Se ci pensate, una ricerca non è altro che una visione parziale di una raccolta. Invece delle auto / ricerca? Q = tipo di URL BMW, dovremmo vedere più auto? Produttore = BMW.


Intendevi 416 "Intervallo richiesto non soddisfacente" o "413" Entità richiesta troppo grande?

1
@Mohamed Penso che intendi If-Unmodified-Since, che corrisponde alla variante E-Tag If-Match, piuttosto che If-Modified-Since. Detto questo, potresti anche considerare di rimuovere questo vincolo, a seconda del tuo caso d'uso. Supponi di avere una raccolta che cresce solo dall'alto (come una raccolta di stile "più recente prima"), il peggio che può accadere se tale raccolta cambia tra le richieste è che un utente che sfoglia una raccolta vede le voci due volte. (Che di per sé è anche un'informazione utile: dice all'utente che la collezione è cambiata)
Eugene Beresovsky

20
413 è "Richiesta entità troppo grande", non "Entità richiesta troppo grande". Significa che la dimensione della tua richiesta, ad esempio quando si carica un file, è maggiore di quella che il server è disposto a elaborare. Quindi usarlo per questo non sembra essere del tutto appropriato.
user247702

@Mohamed So che è una vecchia domanda ma se l'ETag di una raccolta è l'ETag della risorsa modificata più di recente che contiene la raccolta, quale valore dell'intestazione If-Match dovrebbe essere usato quando si modifica una risorsa nella raccolta? L'uso del valore dell'ETag restituito con la raccolta è errato poiché il client sarebbe in grado di modificare la risorsa anche se non vede l'ultimo stato della risorsa.
Mickael Marrache,

8
Non sono assolutamente d'accordo sull'uso 413. Questo è un codice di errore che indica che il client sta inviando qualcosa che il server rifiuta di accettare a causa delle dimensioni. Non il contrario! Vedi tools.ietf.org/html/rfc7231#section-6.5.11 (nota che dice payload richiesta . Non payload risposta )!
exhuma,

7

Puoi comunque tornare Accept-Rangese Content-Rangescon un 200codice di risposta. Queste due intestazioni di risposta forniscono informazioni sufficienti per dedurre le stesse informazioni 206fornite in modo esplicito da un codice di risposta.

Vorrei usare Rangeper l'impaginazione, e ho semplicemente restituire a 200per una pianura GET.

Questo sembra 100% RESTful e non rende la navigazione più difficile.

Modifica: ho scritto un post sul blog su questo: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html


5

Se esiste più di una pagina di risposte e non vuoi offrire l'intera raccolta in una sola volta, significa che ci sono più scelte?

Su una richiesta /db/questions, torna 300 Multiple Choicescon le Linkintestazioni che specificano come raggiungere ciascuna pagina, nonché un oggetto JSON o una pagina HTML con un elenco di URL.

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

Avresti Linkun'intestazione per ogni pagina di risultati (una stringa vuota indica l'URL corrente e l'URL è lo stesso per ogni pagina, appena accessibile con intervalli diversi) e la relazione è definita come personalizzata per la Linkspecifica imminente . Questa relazione spiegherebbe la tua abitudine 266o la tua violazione di 206. Queste intestazioni sono la tua versione leggibile automaticamente, poiché tutti i tuoi esempi richiedono comunque un client comprensivo.

(Se segui il percorso "range", credo che il tuo 2xxcodice di ritorno, come lo hai descritto, sarebbe il comportamento migliore qui. Dovresti farlo per le tue applicazioni e tali ["codici di stato HTTP sono estensibili. "] e hai buone ragioni.)

300 Multiple Choicesdice che DOVREBBE anche fornire a un corpo un modo per l'agente utente di scegliere. Se il tuo cliente capisce, dovrebbe usare le Linkintestazioni. Se si tratta di un utente che naviga manualmente, forse una pagina HTML con collegamenti a una speciale risorsa radice "paginata" in grado di gestire il rendering di quella particolare pagina in base all'URL? /humanpage/1/db/questionso qualcosa di così orribile?


I commenti sul post di Richard Levasseur mi ricordano un'opzione aggiuntiva: l' Acceptintestazione (sezione 14.1). Quando sono uscite le specifiche oEmbed, mi chiedevo perché non fosse stato fatto interamente con HTTP e ho scritto un'alternativa usando loro.

Tenere le 300 Multiple Choices, le Linkintestazioni e la pagina HTML per un HTTP ingenua iniziale GET, ma invece di utilizzare le gamme, avere il vostro nuovo rapporto di paging definire l'uso della Accepttestata. La tua successiva richiesta HTTP potrebbe apparire così:

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

L' Acceptintestazione ti consente di definire un tipo di contenuto accettabile (il tuo ritorno JSON), oltre a parametri estensibili per quel tipo (il tuo numero di pagina). Riffing sulle mie note dal mio oEmbed writeup (non posso collegarmi qui, lo elencherò nel mio profilo), potresti essere molto esplicito e fornire una versione specifica / relazione qui nel caso in cui sia necessario ridefinire il pagesignificato del parametro nel futuro.


1
+1 per le intestazioni dei collegamenti, ma raccomanderei anche le prime, precedenti, successive, ultime rel comuni, nonché l'archivio precedente, l'archivio successivo e l'attuale di RFC5005.
Joseph Holsten,

> Su una richiesta a / db / domande, restituisci 300 scelte multiple con intestazioni Link che specificano come accedere a ciascuna pagina [..] Il problema (e con i progetti REST più puri) è che sta uccidendo per latenza. L'obiettivo è ridurre al minimo le richieste di rete. Quella prima richiesta dovrebbe produrre risultati, non collegamenti a più richieste che alla fine forniranno i dati di cui abbiamo bisogno.
Stijn de Witt,

4

Modificare:

Dopo averci pensato un po 'di più, sono propenso a concordare sul fatto che le intestazioni Range non sono appropriate per l'impaginazione. Essendo la logica, l'intestazione Range è destinata alla risposta del server, non alle applicazioni. Se hai fornito 100 megabyte di risultati, ma il server (o client) poteva elaborare solo 1 megabyte alla volta, beh, ecco a cosa serve l'intestazione Range.

Sono anche dell'opinione che un sottoinsieme di risorse sia la propria risorsa (simile all'algebra relazionale), quindi merita una rappresentazione nell'URL.

Quindi, fondamentalmente, ho ritrattato la mia risposta originale (di seguito) sull'utilizzo di un'intestazione.


Penso che tu abbia risposto alla tua domanda, più o meno: restituisci 200 o 206 con la gamma di contenuti e opzionalmente usa un parametro di query. Annuserei l'agente utente e il tipo di contenuto e, a seconda di questi, verificherei un parametro di query. Altrimenti, richiedono le intestazioni di intervallo.

Hai essenzialmente obiettivi contrastanti: consentire alle persone di utilizzare il proprio browser per esplorare (che non consente facilmente intestazioni personalizzate) o forzare le persone a utilizzare un client speciale in grado di impostare intestazioni (che non consente loro di esplorare).

Potresti semplicemente fornire loro il client speciale a seconda della richiesta: se sembra un semplice browser, invia una piccola app Ajax che esegue il rendering della pagina e imposta le intestazioni necessarie.

Naturalmente, c'è anche il dibattito sul fatto che l'URL debba contenere tutto lo stato necessario per questo genere di cose. Determinare l'intervallo usando le intestazioni può essere considerato "non riposante" da alcuni.

A parte questo, sarebbe bello se i server potessero rispondere con un'intestazione "Can-Specify: Header1, header2" e i browser Web presenterebbero un'interfaccia utente in modo che gli utenti possano inserire i valori, se lo desiderano.


Grazie per la risposta. Ho pensato all'argomento, ma speravo di ottenere una seconda opinione. Hai mai avuto un puntatore agli argomenti dell'intestazione?
Karl Guertin,

Ecco l'unico che ho aggiunto ai segnalibri (vedi la discussione nei commenti): barelyenough.org/blog/2008/05/versioning-rest-web-services Un altro sito ruota attorno all'uso di Ruby di .json, .xml, .whats in determinando il tipo di contenuto di una richiesta. Alcuni degli esempi: * lingua - inserendolo nell'URL significa che inviare il link ad un altro paese lo renderebbe nella lingua sbagliata. * impaginazione - Metterlo nell'intestazione significa che non puoi collegare le persone a ciò che vedi
Richard Levasseur,

* tipo di contenuto: una combinazione di problemi di linguaggio e impaginazione - se è nell'URL, cosa succede se il client non supporta quel tipo di contenuto (ad es. un'estensione .ajax e un'estensione .html)? Al contrario, senza quel tipo di contenuto nell'URL, non è possibile garantire che venga fornita la stessa rappresentazione. "nuovo sito ajax! example.com/cool.ajax" vs "articolo interessante qui: esempio.com/article.ajax#id=123".
Richard Levasseur,

2
IMO, se va nell'URL o no dipende da cosa è. La mia regola generale è che, se identificasse una risorsa concreta (sia essa una risorsa in uno stato specifico, una selezione di risorse o un risultato discreto), va nell'URL. Le query di ricerca, l'impaginazione e le transazioni riposanti ne sono un buon esempio. Se è qualcosa che è necessario per trasformare la rappresentazione astratta in una rappresentazione concreta, va nell'intestazione. autenticazione e tipo di contenuto ne sono un buon esempio.
Richard Levasseur,

Penso alla stringa di query in un URL come opzioni per interrogare la risorsa specificata.
wprl,

3

Potresti prendere in considerazione l'utilizzo di un modello come Atom Feed Protocol poiché ha un modello HTTP sano di raccolte e come manipolarle (dove folle significa WebDAV).

C'è il protocollo di pubblicazione Atom che definisce il modello di raccolta e le operazioni REST e in più è possibile utilizzare RFC 5005 - Feed paging e archiviazione per sfogliare le grandi raccolte.

Il passaggio dal contenuto Atom XML al contenuto JSON non dovrebbe influire sull'idea.


3

Penso che il vero problema qui sia che non c'è nulla nelle specifiche che ci dice come fare reindirizzamenti automatici di fronte a 413 - Entità richiesta troppo grande.

Recentemente stavo lottando con questo stesso problema e ho cercato ispirazione nel libro RESTful Web Services . Personalmente non credo che 206 sia appropriato a causa del requisito dell'intestazione. Anche i miei pensieri mi hanno portato a 300, ma ho pensato che fosse più per i diversi tipi di mime, quindi ho cercato cosa avevano da dire Richardson e Ruby sull'argomento nell'Appendice B, pagina 377. Suggeriscono che il server scelga semplicemente il preferito rappresentazione e rispedirlo con un 200, sostanzialmente ignorando l'idea che dovrebbe essere un 300.

Ciò si fonde anche con la nozione di collegamenti alle risorse successive che abbiamo da Atom. La soluzione che ho implementato è stata quella di aggiungere le chiavi "next" e "precedente" alla json map che stavo rinviando e averla fatta.

Più tardi ho iniziato a pensare che forse la cosa da fare è inviare un 307 - Reindirizzamento temporaneo a un collegamento che sarebbe qualcosa come / db / questions / 1,25 - che lascia l'URI originale come nome della risorsa canonica, ma ti porta a una risorsa subordinata opportunamente denominata. Questo è un comportamento che mi piacerebbe vedere da un 413, ma 307 sembra un buon compromesso. In realtà non l'ho ancora provato nel codice. Ciò che sarebbe ancora meglio è che il reindirizzamento reindirizzasse a un URL contenente gli ID effettivi delle domande più recenti. Ad esempio, se ogni domanda ha un ID intero e ci sono 100 domande nel sistema e si desidera mostrare le dieci più recenti, le richieste a / db / domande dovrebbero essere 307 a / db / questions / 100,91

Questa è un'ottima domanda, grazie per averlo chiesto. Mi hai confermato che non sono pazzo per aver passato giorni a pensarci.


303 sarebbe migliore a questo proposito di 307. 307 implica che l'URL originale inizierà presto a rispondere come previsto dal client.
Nicholas Shanks,

RFC 7231 fa riferimento al codice di stato HTTP 413 come payload troppo grande e mette in relazione questo codice con la dimensione della richiesta e non con la potenziale risposta.
beawolf,

1

Puoi rilevare l' Rangeintestazione e imitare Dojo se è presente e imitare Atom se non lo è. Mi sembra che questo divida ordinatamente i casi d'uso. Se stai rispondendo a una query REST dalla tua applicazione, ti aspetti che sia formattato con Rangeun'intestazione. Se stai rispondendo a un browser casuale, quindi se restituisci collegamenti di paging, lo strumento fornirà un modo semplice per esplorare la raccolta.


1

Uno dei maggiori problemi con le intestazioni di gamma è che molti proxy aziendali li filtrano. Consiglierei invece di utilizzare un parametro di query.



0

Mi sembra che il modo migliore per farlo sia includere l'intervallo come parametri di query. ad es., GET / db / questions /? date> mindate & date <maxdate . Dopo un GET su / db / questions / senza parametri di query, restituire 303 con Posizione: / db / questions /? Parametri-query-per-recuperare-la-pagina-predefinita . Quindi fornire un URL diverso in base al quale chiunque stia utilizzando l'API per ottenere statistiche sulla raccolta (ad esempio, quali parametri di query utilizzare se desidera l'intera raccolta);


0

Sebbene sia possibile utilizzare l'intestazione Range per questo scopo, non penso che questo fosse l'intento. Sembra che sia stato progettato per gestire connessioni instabili e per limitare i dati (quindi il client può richiedere parte della richiesta se mancava qualcosa o se la dimensione era troppo grande per l'elaborazione). Stai hackerando l'impaginazione in qualcosa che è probabilmente usato per altri scopi a livello di comunicazione. Il modo "corretto" di gestire l'impaginazione è con i tipi restituiti. Invece di restituire l'oggetto domande, dovresti invece restituire un nuovo tipo.

Quindi se le domande sono così:

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

Il nuovo tipo potrebbe essere qualcosa del genere:

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

Ovviamente controlli i tuoi tipi di media, così puoi rendere le tue "pagine" un formato adatto alle tue esigenze. Se si crea qualcosa di generico, è possibile avere un unico parser sul client per gestire lo stesso paging per tutti i tipi. Penso che sia più nello spirito della specifica HTTP, piuttosto che confondere il parametro Range per qualcos'altro.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.