Progettazione di un'API REST mediante URI vs stringa di query


73

Diciamo che ho tre risorse che sono correlate in questo modo:

Grandparent (collection) -> Parent (collection) -> and Child (collection)

Quanto sopra descrive la relazione tra queste risorse in questo modo: ogni nonno può mappare a uno o più genitori. Ogni genitore può mappare uno o più figli. Voglio la capacità di supportare la ricerca rispetto alla risorsa figlio ma con i criteri di filtro:

Se i miei clienti mi passano un riferimento identificativo a un nonno, voglio solo cercare i bambini che sono discendenti diretti di quel nonno.

Se i miei clienti mi passano un riferimento ID a un genitore, voglio solo cercare i figli che sono discendenti diretti del mio genitore.

Ho pensato a qualcosa del genere:

GET /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text}

e

GET /myservice/api/v1/parents/{parentID}/children?search={text}

per i requisiti di cui sopra, rispettivamente.

Ma potrei anche fare qualcosa del genere:

GET /myservice/api/v1/children?search={text}&grandparentID={id}&parentID=${id}

In questo progetto, potrei consentire al mio client di passare l'uno o l'altro nella stringa di query: grandparentID o parentID, ma non entrambi.

Le mie domande sono:

1) Quale design API è più RESTful e perché? Semanticamente, significano e si comportano allo stesso modo. L'ultima risorsa nell'URI è "figli", il che implica effettivamente che il client sta operando sulla risorsa figlio.

2) Quali sono i pro e i contro di ciascuno in termini di comprensibilità dal punto di vista del cliente e manutenibilità dal punto di vista del progettista.

3) A cosa servono realmente le stringhe di query, oltre al "filtro" sulla tua risorsa? Se si segue il primo approccio, il parametro filter viene incorporato nell'URI stesso come parametro path anziché come parametro della stringa di query.

Grazie!


3
Il titolo della tua domanda dovrebbe essere estremamente confuso per chiunque lo veda. I segmenti validi di un URI sono definiti come <scheme>: // <user>: <password> @ <host>: <port> / <path>; <params>? <query> / # <fragment> (sebbene < password> è obsoleta) Una "stringa di query" è un componente valido di un URI quindi il tuo "vs" nel titolo è un discorso folle.
K. Alan Bates,

Intendi I want to only search against children who are INdirect descendants of that grandparent.? Secondo la tua struttura, il nonno non ha figli diretti.
null

Qual è la differenza tra un bambino e un genitore? Un genitore è un genitore se non ha figli? Odori di un difetto di progettazione
Pinoniq

ri: potential design flawe se hai informazioni su una persona ma nessuna informazione sui loro genitori, si qualificano come child? (ad es. Adamo ed Eva) :)
Jesse Chisholm,

Risposte:


64

Primo

Secondo RFC 3986 §3.4 (Identificatori di risorse uniformi § (Componenti di sintassi) | Query

3.4 Query

Il componente di query contiene dati non gerarchici che, insieme ai dati nel componente di percorso (Sezione 3.3), servono a identificare una risorsa nell'ambito dello schema dell'URI e dell'autorità di denominazione (se presente).

I componenti di query servono per il recupero di dati non gerarchici; ci sono poche cose più gerarchiche in natura di un albero genealogico! Ergo - indipendentemente dal fatto che si pensi che sia "REST-y" o no- per conformarsi ai formati, protocolli e framework di e per lo sviluppo di sistemi su Internet, non è necessario utilizzare la stringa di query per identificare queste informazioni.

REST non ha nulla a che fare con questa definizione.

Prima di rispondere a domande specifiche, il parametro della query di "ricerca" è scarsamente denominato. Meglio sarebbe trattare il tuo segmento di query come un dizionario di coppie chiave-valore.

La stringa di query potrebbe essere definita in modo più appropriato come

?first_name={firstName}&last_name={lastName}&birth_date={birthDate} eccetera.

Per rispondere alle tue domande specifiche

1) Quale design API è più RESTful e perché? Semanticamente, significano e si comportano allo stesso modo. L'ultima risorsa nell'URI è "figli", il che implica effettivamente che il client sta operando sulla risorsa figlio.

Non penso che sia così chiaro come sembra credere.

Nessuna di queste interfacce di risorse è RESTful. Il presupposto principale per lo stile architettonico RESTful è che le transizioni dello stato dell'applicazione devono essere comunicate dal server come hypermedia. Le persone hanno lavorato sulla struttura degli URI per renderli in qualche modo "URI RESTful", ma la letteratura formale relativa al REST in realtà ha ben poco da dire al riguardo. La mia opinione personale è che gran parte della meta-disinformazione su REST è stata pubblicata con l'intento di rompere vecchie, cattive abitudini. (Costruire un sistema veramente "RESTful" è in realtà un bel po 'di lavoro. L'industria è passata a "REST" e ha riempito alcune preoccupazioni ortogonali con qualifiche e restrizioni insensate.)

Ciò che la letteratura REST dice è che se si intende utilizzare HTTP come protocollo dell'applicazione, è necessario rispettare i requisiti formali delle specifiche del protocollo e non è possibile "inventare http mentre si procede e dichiarare comunque che si sta utilizzando http" ; se si intende utilizzare gli URI per identificare le risorse, è necessario rispettare i requisiti formali delle specifiche relative a URI / URL.

La tua domanda è indirizzata direttamente da RFC3986 §3.4, che ho collegato sopra. La linea di fondo su questo argomento è che anche se un URI conforme non è sufficiente per considerare un API "riposante", se si desidera che il sistema in realtà essere "RESTful" e si utilizza HTTP e URI, quindi non è possibile identificare i dati gerarchici attraverso la stringa di query perché:

3.4 Query

Il componente di query contiene dati non gerarchici

...E 'così semplice.

2) Quali sono i pro e i contro di ciascuno in termini di comprensibilità dal punto di vista del cliente e manutenibilità dal punto di vista del progettista.

I "pro" dei primi due sono che sono sulla buona strada . Il "contro" del terzo è che sembra essere completamente sbagliato.

Per quanto riguarda la comprensibilità e la manutenibilità, questi sono decisamente soggettivi e dipendono dal livello di comprensione dello sviluppatore del cliente e dalle costanti progettuali del progettista. La specifica URI è la risposta definitiva su come si suppone che gli URI debbano essere formattati. I dati gerarchici dovrebbero essere rappresentati nel percorso e con i parametri del percorso. I dati non gerarchici dovrebbero essere rappresentati nella query. Il frammento è più complicato, poiché la sua semantica dipende specificamente dal tipo di supporto della rappresentazione richiesta. Quindi, per affrontare la componente "comprensibilità" della tua domanda, cercherò di tradurre esattamente ciò che i tuoi primi due URI stanno effettivamente dicendo. Quindi, tenterò di rappresentare ciò che dici che stai cercando di realizzare con URI validi.

Traduzione dei tuoi URI verbali al loro significato semantico /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text} Questo dice per i genitori dei nonni, trova il loro bambino avere search={text} quello che hai detto con il tuo URI è coerente solo se cerca i fratelli di un nonno. Con i tuoi "nonni, genitori, figli" hai scoperto che un "nonno" è cresciuto di generazione in generazione dai loro genitori e poi è tornato alla generazione di "nonni" guardando i figli dei genitori.

/myservice/api/v1/parents/{parentID}/children?search={text} Questo dice che per il genitore identificato da {parentID}, trova il figlio che ha ?search={text}Questo è più vicino alla correzione di ciò che desideri e rappresenta una relazione genitore-> figlio che può essere probabilmente usata per modellare l'intera API. Per modellarlo in questo modo, l'onere è a carico del cliente per riconoscere che se hanno un "grandparentId", esiste un livello di riferimento indiretto tra l'ID che hanno e la parte del grafico familiare che desiderano vedere. Per trovare un "bambino" in "grandparentId", puoi chiamare il tuo /parents/{parentID}/childrenservizio e quindi cercare un figlio che viene restituito, cerca i loro figli per l'identificatore della persona.

Implementazione dei tuoi requisiti come URI Se vuoi modellare un identificatore di risorse più estensibile che possa camminare sull'albero, posso pensare a diversi modi in cui puoi farlo.

1) Il primo a cui ho già accennato. Rappresenta il grafico di "Persone" come una struttura composita. Ogni persona ha un riferimento alla generazione sopra di essa attraverso il suo percorso dei genitori e a una generazione sotto di essa attraverso il suo percorso dei figli.

/Persons/Joe/Parents/Mother/Parents sarebbe un modo per prendere i nonni materni di Joe.

/Persons/Joe/Parents/Parents sarebbe un modo per prendere tutti i nonni di Joe.

/Persons/Joe/Parents/Parents?id={Joe.GrandparentID} afferrerebbe il nonno di Joe con l'identificatore che hai in mano.

e tutti questi avrebbero senso (notare che qui potrebbe esserci una penalità per le prestazioni in base all'attività forzando un dfs sul server a causa della mancanza di identificazione del ramo nel modello "Genitori / Genitori / Genitori"). la capacità di supportare qualsiasi numero arbitrario di generazioni. Se, per qualche motivo, desideri cercare 8 generazioni, potresti rappresentarlo come

/Persons/Joe/Parents/Parents/Parents/Parents/Parents/Parents/Parents/Parents?id={Joe.NotableAncestor}

ma questo porta alla seconda opzione dominante per rappresentare questi dati: attraverso un parametro path.


2) Usa i parametri del percorso per "interrogare la gerarchia". Potresti sviluppare la seguente struttura per aiutare ad alleviare l'onere per i consumatori e avere ancora un'API che abbia senso.

Per guardare indietro di 147 generazioni, rappresentando questo identificatore di risorsa con i parametri del percorso è possibile farlo

/Persons/Joe/Parents;generations=147?id={Joe.NotableAncestor}

Per localizzare Joe dal suo bisnonno, puoi guardare nel grafico un numero noto di generazioni per l'ID di Joe. /Persons/JoesGreatGrandparent/Children;generations=3?id={Joe.Id}

La cosa più importante di questi approcci è che senza ulteriori informazioni nell'identificatore e nella richiesta, dovresti aspettarti che il primo URI stia recuperando una persona di 147 generazioni da Joe con l'identificatore di Joe.NotableAncestor. Dovresti aspettarti che il secondo recuperi Joe. Supponiamo che ciò che desideri effettivamente sia che il tuo client chiamante sia in grado di recuperare l'intero set di nodi e le loro relazioni tra la persona root e il contesto finale del tuo URI. È possibile farlo con lo stesso URI (con qualche decorazione aggiuntiva) e impostando Accetta text/vnd.graphvizsulla richiesta, che è il tipo di supporto registrato IANA per la .dotrappresentazione grafica. Con ciò, cambia l'URI in

/Persons/Joe/Parents;generations=147?id={Joe.NotableAncestor.Id}#directed

con un'intestazione di richiesta HTTP Accept: text/vnd.graphviz e puoi avere i clienti che comunicano abbastanza chiaramente che vogliono il grafico diretto della gerarchia generazionale tra Joe e 147 generazioni precedenti, dove quella 147a generazione ancestrale contiene una persona identificata come "Notable Ancestor" di Joe.

Non sono sicuro che text / vnd.graphviz abbia semantiche predefinite per il suo frammento; non sono riuscito a trovarne nessuna in una ricerca di istruzioni. Se quel tipo di supporto contiene effettivamente informazioni predefinite sui frammenti, è necessario seguirne la semantica per creare un URI conforme. Tuttavia, se tali semantiche non sono predefinite, la specifica URI afferma che la semantica dell'identificatore di frammento non è vincolata e viene invece definita dal server, rendendo valido questo utilizzo.


3) A cosa servono realmente le stringhe di query, oltre al "filtro" sulla tua risorsa? Se si segue il primo approccio, il parametro filter viene incorporato nell'URI stesso come parametro path anziché come parametro della stringa di query.

Credo di averlo già completamente battuto a morte, ma le stringhe di query non servono per "filtrare" le risorse. Sono per identificare la tua risorsa da dati non gerarchici. Se hai approfondito la tua gerarchia con il tuo percorso andando /person/{id}/children/e desideri identificare un figlio specifico o un insieme specifico di figli, utilizzeresti alcuni attributi che si applicano all'insieme che stai identificando e lo includeresti all'interno della query.


1
L'RFC si occupa della gerarchia solo nella misura in cui definisce una sintassi e un algoritmo per la risoluzione dei riferimenti URI relativi. Potresti elaborare o citare alcune fonti che spiegano perché gli esempi nel post originale non sono conformi?
user2313838

2
Un albero genealogico non è davvero un grafico, non è un albero, né è affatto gerarchico. considerando più genitori, il divorzio e il nuovo matrimonio ecc.
Misteri il

1
@RobertoAloi Mi sembra controintuitivo comunicare la tua interfaccia "Nessun elemento trovato" attraverso un set vuoto quando HTTP ha già una definizione per questo. Il principio di base è che stai chiedendo al server di restituire "cose" e se non ci sono "cose" da restituire, il server comunica che con "404 - Non trovato" Cosa c'è di controintuitivo in questo?
K. Alan Bates,

1
Ho sempre creduto che 404 indicasse che l'URL radice della risorsa non è stato trovato, ovvero la raccolta nel suo insieme. Quindi, se hai fatto una query / libri? Autore = Jim e non c'erano libri di Jim, riceveresti un set vuoto []. Ma se hai richiesto / articoli? Autore = Jim ma la risorsa degli articoli non esisteva nemmeno in quanto una raccolta 404 aiuterebbe a indicare che non è affatto utile cercare alcun articolo.
aggiorna il

1
@adjenks Non l'hai imparato dalle specifiche formali. Strutturalmente, si può pensare che i costituenti di un url contengano una "radice" se ciò aiuta a ragionare sugli scopi delle parti costituenti, ma alla fine la stringa di query non è un filtro di visualizzazione rispetto a una risorsa identificata tramite il percorso. La stringa di query è un cittadino di prima classe di un URL. Quando non trovi risorse sul server che corrispondono al tuo url (inclusa la stringa di query) 404 è il mezzo definito in http per comunicarlo. Il modello di interazione che poni introduce una distinzione senza differenze.
K. Alan Bates,

14

Qui è dove sbagli:

Se i miei clienti mi passano un riferimento ID

In un sistema REST, il client non dovrebbe mai essere disturbato dagli ID. Gli unici identificatori di risorse che il client dovrebbe conoscere dovrebbero essere gli URI. Questo è il principio di "interfaccia uniforme".

Pensa a come i client interagirebbero con il tuo sistema. Supponiamo che l'utente inizi a sfogliare un elenco di nonni, ha scelto uno dei figli del nonno, che lo porta a /grandparent/123. Se il client dovrebbe essere in grado di cercare i figli di /grandparent/123, quindi in base a "HATEOAS", tutto ciò che viene restituito quando si esegue una query su /grandparent/123dovrebbe restituire un URL all'interfaccia di ricerca. Questo URL dovrebbe già avere tutti i dati necessari per filtrare dall'attuale nonno incorporato in esso.

Se il collegamento si presenta come /grandparent/123?search={term}o /parent?grandparent=123&search={term}o /parent?grandparentTerm=someterm&someothergplocator=blah&search={term}sono irrilevanti in base al REST. Nota come tutti questi URL abbiano lo stesso numero di parametri, il che è {term}, anche se usano criteri diversi. Puoi passare da uno qualsiasi di questi URL o puoi confonderli a seconda dei nonni specifici e il client non si spezzerebbe, perché la relazione logica tra le risorse è la stessa anche se l'implementazione sottostante potrebbe differire in modo significativo.

Se invece hai creato il servizio in modo tale da richiedere /grandparent/{grandparentID}?search={term}quando vai in un modo ma /children?parent={parentID}&search={term}a} quando vai in un altro modo, è troppo accoppiamento perché il cliente dovrebbe sapere di interpolare cose diverse su relazioni diverse concettualmente simili.

Indipendentemente dal fatto che tu vada effettivamente con /grandparent/123?search={term}o /parent?grandparent=123&search={term}è una questione di gusti e qualunque implementazione sia più facile per te in questo momento. L'importante è non richiedere la modifica del client se si modifica la strategia URL o se si utilizzano strategie diverse nelle diverse relazioni genitori-figli.


10

Non sono sicuro del motivo per cui la gente pensa che inserire i valori ID nell'URL significhi in qualche modo un'API REST, REST riguarda la gestione dei verbi, il passaggio di risorse.

Quindi, se vuoi mettere un nuovo utente, dovresti inviare una buona parte di dati e una richiesta POST http è l'ideale, quindi anche se potresti inviare la chiave (es. ID utente), invierai i dati dell'utente (ad es. nome, indirizzo) come dati POST.

Ora è un linguaggio comune mettere l'identificatore di risorsa nell'URI, ma questa è più una convenzione di qualsiasi forma canonica "non è REST se non è nell'URI". Ricorda che la tesi originale di REST in realtà non menziona affatto http, è un idioma per la gestione dei dati in un client-server, non qualcosa che è un'estensione di http (sebbene, ovviamente, http sia la nostra forma principale di implementazione di REST) .

Ad esempio, Fielding utilizza la richiesta di un documento accademico come esempio. Volete recuperare la risorsa "Dr John's Paper sul bere delle birre", ma potreste anche volere la versione iniziale o l'ultima versione, quindi l'identificatore di risorsa potrebbe non essere qualcosa a cui si fa facilmente riferimento come un singolo ID che può essere inserito nell'URI. REST lo consente e un'intenzione dichiarata è:

REST si basa invece sull'autore che sceglie un identificatore di risorsa che si adatti meglio alla natura del concetto identificato

Quindi non c'è nulla che ti impedisca di utilizzare un URI statico per recuperare i tuoi genitori, passando un termine di ricerca nella stringa di query per identificare l'utente esatto che stai cercando. In questo caso, la "risorsa" che stai identificando è l'insieme di nonni (e quindi l'URI contiene "nonni" come parte dell'URI. REST include il concetto di "dati di controllo" progettato per determinare quale rappresentazione del tuo la risorsa deve essere recuperata - l'esempio fornito in questi è il controllo della cache, ma anche il controllo della versione - quindi la mia richiesta per l'eccellente documento del dottor John può essere perfezionata passando la versione come dati di controllo, non parte dell'URI.

Penso che un esempio di interfaccia REST che di solito non sia menzionato sia SMTP . Quando si costruisce un messaggio di posta, si inviano verbi (DA, A, ecc.) Con i dati delle risorse per ciascuna parte del messaggio di posta. Questo è RESTful anche se non usa i verbi http, usa il proprio set.

Quindi ... mentre hai bisogno di avere un po 'di identificazione delle risorse nel tuo URI, non deve essere il tuo riferimento id. Questo può essere felicemente inviato come dati di controllo, nella stringa di query o persino nei dati POST. Quello che stai davvero identificando nell'API REST è che stai cercando un figlio, che hai già nel tuo URI.

Quindi, a mio avviso, leggendo la definizione di REST, stai richiedendo una risorsa figlio, passando i dati di controllo (sotto forma di querystring) per determinare quale desideri restituire. Di conseguenza, non è possibile effettuare una richiesta URI per un nonno o un genitore. Volete che i figli vengano restituiti, quindi il termine genitore / nonno o ID non dovrebbe assolutamente trovarsi in tale URI. I bambini dovrebbero essere.


In realtà, l'URI come concetto è fondamentale per REST. Ciò che non è altrettanto importante è la sintassi dell'URI. Un'interfaccia REST corretta deve identificare le risorse con una sintassi comune. Alla base del principio REST, non è necessariamente male identificare una risorsa complessa con un oggetto JSON invece dell'URI RFC 3986, anche se in tal caso, è necessario utilizzare JSON RI per l'intera API.
Lie Ryan,

4

Molte persone hanno già parlato del significato di REST, ecc. Ma nessuno sembra affrontare il vero problema: il tuo design.

Perché il nonno è diverso da un padre? entrambi hanno figli che possono avere figli che possono ...

Alla fine, sono tutti "umani". Probabilmente lo hai anche nel tuo codice. Quindi usa quello:

GET /<api-endpoint>/humans/<ID>

Restituirà alcune informazioni utili sull'essere umano. (Nome e cose)

GET /<api-endpoint>/humans/<ID>/children

restituirà ovviamente una serie di bambini. Se non esistono figli, un array vuoto è probabilmente la strada da percorrere.

Per semplificare, ad esempio, è possibile aggiungere un flag "hasChildren". o "hasGrandChildren".

Pensa più intelligente, non più difficile


1

Quanto segue è più RESTfull perché ogni grandparentID ottiene il proprio URL. In questo modo la risorsa viene identificata in un modo unico.

GET /myservice/api/v1/grandparents/{grandparentID}
GET /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text}

La ricerca dei parametri di query è un buon modo per eseguire una ricerca dal contesto di quella risorsa.

Quando una famiglia sta diventando molto grande, puoi utilizzare start / limit come opzioni di query, ad esempio:

GET /myservice/api/v1/grandparents/{grandparentID}/children?start=1&limit=50

Come sviluppatore è bene avere risorse diverse con un URL / URI univoci. Penso che dovresti usare il parametro query solo quando potrebbero essere esclusi.

Forse questa è una buona lettura http://www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling e altrimenti la tesi di dottorato originale di Roy T Fielding https://www.ics.uci.edu /~fielding/pubs/dissertation/fielding_dissertation.pdf che spiega i concetti molto bene e completi.


1

Vado con

/myservice/api/v1/people/{personID}/descendants/2?search={text}

In questo caso ogni prefisso è una risorsa valida:

  • /myservice/api/v1/people: tutte le persone
  • /myservice/api/v1/people/{personID}: una persona con informazioni complete (inclusi antenati, fratelli, discendenti)
  • /myservice/api/v1/people/{personID}/descendants: discendenti di una persona
  • /myservice/api/v1/people/{personID}/descendants/2: nipoti di una persona

Potrebbe non essere la risposta migliore, ma almeno ha senso per me.


-1

molti prima di me hanno già sottolineato che il formato dell'URL non ha importanza nel contesto del servizio RESTful ... i miei due centesimi sarebbero ... un aspetto importante di REST come sostenuto nella scrittura originale era il concetto di "Risorse". .quando si progetta l'URL, un aspetto che potrebbe essere necessario tenere presente è che una "risorsa" non è una riga in una singola tabella (anche se potrebbe essere) ... piuttosto è una rappresentazione ... che è anche coerente ... .e può essere utilizzato per apportare modifiche allo stato di tale rappresentazione (ed efficacemente al supporto di archiviazione di base) .. e una determinata risorsa può essere significativa solo nel contesto aziendale ... ad esempio;

Nel tuo esempio / myservice / api / v1 / grandparents / {grandparentID} / genitori / figli? Search = {text}

Potrebbe essere una risorsa più significativa se si accorcia questo / myservice / api / v1 / siblingsOfGrandParents? Search = ..

in questo caso puoi dichiarare 'siblingsOfGrandParents' come risorsa..questo semplifica l'URL

Come altri hanno sottolineato, c'è una diffusa idea sbagliata che è necessario adattarsi a ogni tipo di relazione gerarchica tra oggetti di domini in forma più esplicita in un URL..non esiste una regola dura e veloce per avere una mappatura 1 a 1 tra oggetti di dominio e risorse e rappresentano tutte le loro relazioni ... la domanda dovrebbe piuttosto essere, credo ... c'è bisogno di esporre tale relazione tramite URL ... in particolare segmenti di percorso ... forse no.


quando trascorri il tuo prezioso tempo a declassare una risposta, sarebbe utile se tu potessi parlare e dare le tue ragioni.
Senthu Sivasambu,

Non sono del tutto sicuro del motivo per cui è stato effettuato il downgrade in base alla lettura della risposta. Sono d'accordo con te. Alcune cose immediate che mi saltano fuori sono che hai fornito un esempio leggermente tangente citando "fratelli" quando l'OP stava chiedendo delle relazioni gerarchiche. Forse anche il tuo stile di scrittura potrebbe essere migliorato. Usi un sacco di "...", tendiamo a non usare molto "..." nella scrittura formale, professionale o istruttiva. C'è anche un po 'di ridondanza (ad esempio, menzionate ad esempio, quindi dite nel vostro esempio). Forse la tua risposta potrebbe essere più concisa e rispondere più direttamente alla domanda dei PO.
KennyCason il
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.