Qual è il miglior metodo RESTful per restituire il numero totale di elementi in un oggetto?


139

Sto sviluppando un servizio API REST per un grande sito Web di social network in cui sono coinvolto. Finora funziona alla grande. Posso emettere GET, POST, PUTe DELETEle richieste di URL di oggetti e influenzare i miei dati. Tuttavia, questi dati sono paginati (limitati a 30 risultati alla volta).

Tuttavia, quale sarebbe il miglior modo RESTful per ottenere il numero totale di dire, membri, tramite la mia API?

Attualmente, invio richieste a una struttura URL come la seguente:

  • / api / members: restituisce un elenco di membri (30 alla volta, come indicato sopra)
  • / api / members / 1: influisce su un singolo membro, a seconda del metodo di richiesta utilizzato

La mia domanda è: come dovrei quindi utilizzare una struttura URL simile per ottenere il numero totale di membri nella mia applicazione? Ovviamente richiedere solo il idcampo (simile all'API Graph di Facebook) e contare i risultati sarebbe inefficace dato che solo una porzione di 30 risultati verrebbe restituita.


Risposte:


84

Mentre la risposta a / API / utenti è paginata e restituisce solo 30 record, non c'è nulla che ti impedisca di includere nella risposta anche il numero totale di record e altre informazioni rilevanti, come le dimensioni della pagina, il numero di pagina / offset, ecc. .

L'API StackOverflow è un buon esempio di quello stesso design. Ecco la documentazione per il metodo Users - https://api.stackexchange.com/docs/users


3
+1: Sicuramente la cosa più RESTful da fare se verranno imposti limiti di recupero.
Donal Fellows,

2
@bzim Sapresti che c'è una prossima pagina da recuperare perché c'è un link con rel = "next".
Darrel Miller,

4
@Donal il "prossimo" rel è registrato con IANA iana.org/assignments/link-relations/link-relations.txt
Darrel Miller,

1
@Darrel - sì, potrebbe essere fatto con qualsiasi tipo di flag "next" nel payload. Sento solo che avere il conteggio totale degli elementi della raccolta nella risposta è prezioso da solo e funziona allo stesso modo come una bandiera "successiva".
Franci Penov,

5
Restituire un oggetto che non è un elenco di elementi non è un'implementazione corretta di un'API REST ma REST non fornisce alcun modo per ottenere un elenco parziale dei risultati. Quindi, per rispettare ciò, penso che dovremmo usare le intestazioni per trasmettere altre informazioni come totale, token della pagina successiva e token della pagina precedente. Non l'ho mai provato e ho bisogno di consigli da altri sviluppatori.
Loenix,

74

Preferisco usare le intestazioni HTTP per questo tipo di informazioni contestuali.

Per il numero totale di elementi uso X-total-countintestazione.
Per i collegamenti alla pagina successiva, precedente ecc. Uso l' Linkintestazione http :
http://www.w3.org/wiki/LinkHeader

Github fa allo stesso modo: https://developer.github.com/v3/#pagination

Secondo me è più pulito poiché può essere utilizzato anche quando si restituiscono contenuti che non supportano collegamenti ipertestuali (ad esempio binari, immagini).


5
RFC6648 depreca la convenzione di prefissare i nomi di parametri non standardizzati con la stringa X-.
JDawg,

70

Ultimamente ho svolto alcune ricerche approfondite su questa e altre domande relative al paging REST e ho ritenuto costruttivo aggiungere alcune delle mie scoperte qui. Sto espandendo un po 'la domanda per includere le riflessioni sul paging e il conteggio poiché sono intimamente correlati.

intestazioni

I metadati di paging sono inclusi nella risposta sotto forma di intestazioni di risposta. Il grande vantaggio di questo approccio è che lo stesso payload di risposta è solo l'effettivo richiedente dei dati che stava chiedendo. Semplificare l'elaborazione della risposta per i client che non sono interessati alle informazioni di paging.

Esistono un sacco di intestazioni (standard e personalizzate) utilizzate in natura per restituire informazioni relative al paging, incluso il conteggio totale.

X-Total-Count

X-Total-Count: 234

Questo è usato in alcune API che ho trovato in natura. Esistono anche pacchetti NPM per l'aggiunta del supporto per questa intestazione, ad esempio Loopback. Alcuni articoli raccomandano di impostare anche questa intestazione.

Viene spesso utilizzato in combinazione con l' Linkintestazione, che è una soluzione abbastanza buona per il paging, ma manca delle informazioni sul conteggio totale.

collegamento

Link: </TheBook/chapter2>;
      rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>;
      rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

Sento, leggendo molto su questo argomento, che il consenso generale è quello di utilizzare l' Linkintestazione per fornire collegamenti di paginazione ai clienti che usano rel=next, rel=previousecc. Il problema è che manca l'informazione di quanti record totali ci sono, che è perché molte API combinano questo con l' X-Total-Countintestazione.

In alternativa, alcune API, ad esempio lo standard JsonApi , utilizzano il Linkformato, ma aggiungono le informazioni in una busta di risposta anziché in un'intestazione. Ciò semplifica l'accesso ai metadati (e crea un luogo per aggiungere le informazioni sul conteggio totale) a scapito della crescente complessità dell'accesso ai dati effettivi stessi (aggiungendo una busta).

Content-Range

Content-Range: items 0-49/234

Promosso da un articolo di blog chiamato Range header, ti scelgo (per impaginazione)! . L'autore è un valido esempio dell'uso delle intestazioni Rangee Content-Rangeper l'impaginazione. Quando leggiamo attentamente la RFC su queste intestazioni, scopriamo che estendere il loro significato oltre intervalli di byte è stato effettivamente anticipato dalla RFC ed è esplicitamente permesso. Se utilizzato nel contesto itemsanziché anziché bytes, l'intestazione Range in realtà ci fornisce un modo sia per richiedere un determinato intervallo di elementi sia per indicare a quale intervallo del risultato totale si riferiscono gli elementi di risposta. Questa intestazione offre anche un ottimo modo per mostrare il conteggio totale. Ed è un vero standard che mappa principalmente uno a uno al paging. È anche usato allo stato brado .

Busta

Molte API, inclusa quella del nostro sito Web di domande e risposte preferito, utilizzano una busta , un involucro attorno ai dati utilizzato per aggiungere meta informazioni sui dati. Inoltre, gli standard OData e JsonApi utilizzano entrambi un inviluppo di risposta.

Il grande svantaggio di questo (imho) è che l'elaborazione dei dati di risposta diventa più complessa poiché i dati reali devono essere trovati da qualche parte nella busta. Inoltre ci sono molti formati diversi per quella busta e devi usare quello giusto. Sta dicendo che le buste di risposta di OData e JsonApi sono molto diverse, con OData che si mescola in metadati in più punti della risposta.

Endpoint separato

Penso che questo sia stato trattato abbastanza nelle altre risposte. Non ho studiato molto perché sono d'accordo con i commenti sul fatto che ciò sia confuso, dato che ora hai più tipi di endpoint. Penso che sia più bello se ogni endpoint rappresenta una (raccolta di) risorse.

Ulteriori pensieri

Non dobbiamo solo comunicare le meta informazioni di paging relative alla risposta, ma anche consentire al cliente di richiedere pagine / intervalli specifici. È interessante esaminare anche questo aspetto per trovare una soluzione coerente. Anche qui possiamo usare le intestazioni (l' Rangeintestazione sembra molto adatta) o altri meccanismi come i parametri di query. Alcune persone sostengono di trattare le pagine dei risultati come risorse separate, il che può avere senso in alcuni casi d'uso (ad es /books/231/pages/52. Ho finito per selezionare una gamma selvaggia di parametri di richiesta di uso frequente come pagesize, page[size]ed limitecc. Oltre a supportare l' Rangeintestazione (e come parametro di richiesta anche).


Ero particolarmente interessato all'intestazione Range, tuttavia non sono riuscito a trovare prove sufficienti che l'uso di qualcosa di diverso da bytesun tipo di intervallo sia valido.
VisioN,

2
Penso che la prova più chiara possa essere trovata nella sezione 14.5 della RFC : acceptable-ranges = 1#range-unit | "none"penso che questa formulazione lasci esplicitamente spazio per unità di gamma diverse da quelle bytes, sebbene la specifica stessa definisca solo bytes.
Stijn de Witt,

24

Alternativa quando non hai bisogno di oggetti reali

La risposta di Franci Penov è sicuramente il modo migliore per andare, quindi restituisci sempre gli articoli insieme a tutti i metadati aggiuntivi sulle tue entità richieste. Questo è il modo in cui dovrebbe essere fatto.

ma a volte restituire tutti i dati non ha senso, perché potresti non averne affatto bisogno. Forse tutto ciò di cui hai bisogno sono quei metadati sulla tua risorsa richiesta. Come il conteggio totale o il numero di pagine o qualcos'altro. In tal caso puoi sempre avere una query URL che dice al tuo servizio di non restituire articoli ma piuttosto solo metadati come:

/api/members?metaonly=true
/api/members?includeitems=0

o qualcosa di simile ...


10
Incorporare queste informazioni nelle intestazioni ha il vantaggio di poter fare una richiesta HEAD per ottenere solo il conteggio.
felixfbecker,

1
@felixfbecker esattamente, grazie per reinventare la ruota e ingombrare le API con tutti i tipi di meccanismi diversi :)
EralpB

1
@EralpB Grazie per aver reinventato la ruota e ingombrare le API !? HEAD è specificato in HTTP. metaonlyo includeitemsnon lo è.
felixfbecker,

2
@felixfbecker solo "esattamente" era pensato per te, il resto è per l'OP. Dispiace per la confusione.
EralpB,

REST si basa sull'utilizzo di HTTP e sull'utilizzo per quello per cui è stato progettato il più possibile. In questo caso dovrebbe essere utilizzato il Content-Range (RFC7233). Le soluzioni all'interno del corpo non vanno bene, soprattutto perché non funzionerà con HEAD. la creazione di nuove intestazioni come suggerito qui è inutile e sbagliata.
Vance Shipley,

23

È possibile restituire il conteggio come intestazione HTTP personalizzata in risposta a una richiesta HEAD. In questo modo, se un cliente desidera solo il conteggio, non è necessario restituire l'elenco effettivo e non è necessario un URL aggiuntivo.

(Oppure, se ci si trova in un ambiente controllato da un endpoint all'altro, è possibile utilizzare un verbo HTTP personalizzato come COUNT.)


4
"Intestazione HTTP personalizzata"? Questo sarebbe sotto il titolo di essere un po 'sorprendente, il che a sua volta è contrario a quello che penso dovrebbe essere un'API RESTful. Alla fine, dovrebbe essere sorprendente.
Donal Fellows,

21
@Donal lo so. Ma tutte le buone risposte sono già state prese. :(
bzlm,

1
Lo so anche io, ma a volte devi solo lasciare che altre persone rispondano. O migliora il tuo contributo in altri modi, come una spiegazione dettagliata del perché dovrebbe essere fatto nel modo migliore piuttosto che in altri.
Donal Fellows,

4
In un ambiente controllato, questo potrebbe non sorprendere, dal momento che sarebbe probabilmente usato internamente e basato sulla politica API dei tuoi sviluppatori. Direi che questa è stata una buona soluzione in alcuni casi e che vale la pena avere qui come nota di una possibile soluzione insolita.
James Billingham,

1
Mi piace molto usare le intestazioni HTTP per questo tipo di cose (è davvero dove appartiene). L' intestazione Link standard potrebbe essere appropriata in questo caso (l'API Github lo utilizza).
Mike Marcacci,


7

A partire da "X -" - Il prefisso era obsoleto. (vedi: https://tools.ietf.org/html/rfc6648 )

Abbiamo trovato "Accept-Ranges" come la migliore scommessa per mappare il range di impaginazione: https://tools.ietf.org/html/rfc7233#section-2.3 Poiché le "Unità di portata " possono essere "byte" o " gettone". Entrambi non rappresentano un tipo di dati personalizzato. (vedi: https://tools.ietf.org/html/rfc7233#section-4.2 ) Tuttavia, si afferma che

Le implementazioni HTTP / 1.1 POSSONO ignorare gli intervalli specificati usando altre unità.

Il che indica: l'utilizzo di Range Range personalizzate non è contro il protocollo, ma PUO 'essere ignorato.

In questo modo, dovremmo impostare gli Accept-Ranges su "membri" o qualunque tipo di unità a distanza, ci aspetteremmo. Inoltre, imposta anche l'intervallo di contenuti sull'intervallo corrente. (vedi: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 )

Ad ogni modo, mi atterrerei alla raccomandazione di RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ) di inviare un 206 anziché 200:

Se tutte le condizioni preliminari sono vere, il server supporta il campo di
intestazione Intervallo per la risorsa di destinazione e gli intervalli specificati sono
validi e soddisfacenti (come definito nella Sezione 2.1), il server DOVREBBE
inviare una risposta 206 (Contenuto parziale) con un payload contenente una
o più rappresentazioni parziali che corrispondono agli
intervalli soddisfacenti richiesti, come definito nella Sezione 4.

Di conseguenza, avremmo i seguenti campi di intestazione HTTP:

Per contenuti parziali:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

Per il contenuto completo:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20

3

Sembra più semplice aggiungere semplicemente a

GET
/api/members/count

e restituisce il conteggio totale dei membri


11
Non è una buona idea. Obbligate i clienti a fare 2 richieste per costruire l'impaginazione sulle loro pagine. Prima richiesta per ottenere l'elenco delle risorse e seconda per contare il totale.
Jekis,

Penso che sia un buon approccio ... puoi anche restituire solo un elenco di risultati come json e sul lato client controlla la dimensione della raccolta, quindi un caso del genere è un esempio stupido ... inoltre puoi avere / api / members / count e poi / api / members? offset = 10 & limit = 20
Michał Ziobro

1
Inoltre, tieni presente che molti tipi di impaginazione non richiedono un conteggio (come lo scroll infinito) - Perché calcolarlo quando il cliente potrebbe non averne bisogno
tofarr

2

Che dire di un nuovo end point> / api / members / count che chiama solo Members.Count () e restituisce il risultato


27
Dare al conteggio un endpoint esplicito lo rende una risorsa indirizzabile autonoma. Funzionerà, ma solleverà interessanti domande per chiunque sia nuovo nella tua API. Il conteggio dei membri della raccolta è una risorsa separata dalla raccolta? Posso aggiornarlo con una richiesta PUT? Esiste per una raccolta vuota o solo se ci sono elementi in essa? Se la membersraccolta può essere creata da una richiesta POST a /api, verrà /api/members/countcreata anche come effetto collaterale oppure devo fare una richiesta POST esplicita per crearla prima di richiederla? :-)
Franci Penov,

2

A volte i framework (come $ resource / AngularJS) richiedono un array come risultato di una query, e non si può davvero avere una risposta come {count:10,items:[...]}in questo caso immagazzino "count" in responseHeaders.

PS In realtà puoi farlo con $ resource / AngularJS, ma ha bisogno di alcune modifiche.


Quali sono quelle modifiche? Sarebbero utili su questioni come questa: stackoverflow.com/questions/19140017/...
JBCP

Angolare NON RICHIEDE un array come risultato della query, devi solo configurare la tua risorsa con la proprietà dell'oggetto opzione:isArray: false|true
Rémi Becheras

0

Potresti considerare countscome una risorsa. L'URL sarebbe quindi:

/api/counts/member

-1

Quando si richiedono dati impaginati, si conosce (in base al valore del parametro della dimensione della pagina esplicita o al valore della dimensione della pagina predefinito) la dimensione della pagina, in modo da sapere se si hanno tutti i dati in risposta o meno. Quando ci sono meno dati in risposta di una dimensione di pagina, allora hai tutti i dati. Quando viene restituita una pagina intera, è necessario richiedere nuovamente un'altra pagina.

Preferisco avere un endpoint separato per il conteggio (o lo stesso endpoint con parametro countOnly). Perché è possibile preparare l'utente finale per un processo che richiede molto tempo / tempo mostrando la barra di avanzamento correttamente avviata.

Se si desidera restituire le dimensioni dei dati in ciascuna risposta, è necessario specificare anche PageSize, anche offset. Ad essere onesti, il modo migliore è ripetere anche i filtri di una richiesta. Ma la risposta è diventata molto complessa. Quindi, preferisco un endpoint dedicato per restituire il conteggio.

<data>
  <originalRequest>
    <filter/>
    <filter/>
  </originalReqeust>
  <totalRecordCount/>
  <pageSize/>
  <offset/>
  <list>
     <item/>
     <item/>
  </list>
</data>

Couleage mio, preferisco un parametro countOnly all'endpoint esistente. Quindi, quando specificato, la risposta contiene solo metadati.

endpoint? filter = valore

<data>
  <count/>
  <list>
    <item/>
    ...
  </list>
</data>

endpoint? filter = valore & countOnly = true

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
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.