Quali sono le migliori pratiche per le risorse nidificate REST?


301

Per quanto ne so, ogni singola risorsa dovrebbe avere un solo percorso canonico . Quindi nel seguente esempio quali sarebbero i buoni pattern URL?

Prendiamo ad esempio una rappresentazione di riposo delle aziende. In questo ipotetico esempio, ogni azienda possiede 0 o più dipartimenti e ogni dipartimento possiede 0 o più dipendenti.

Un dipartimento non può esistere senza una società associata.

Un dipendente non può esistere senza un dipartimento associato.

Ora troverei la rappresentazione naturale dei modelli di risorse.

  • /companies Una collezione di aziende : accetta di creare una nuova società. Ottieni per l'intera collezione.
  • /companies/{companyId}Un'azienda individuale. Accetta GET, PUT e DELETE
  • /companies/{companyId}/departmentsAccetta POST per un nuovo oggetto. (Crea un dipartimento all'interno dell'azienda.)
  • /companies/{companyId}/departments/{departmentId}/
  • /companies/{companyId}/departments/{departmentId}/employees
  • /companies/{companyId}/departments/{departmentId}/employees/{empId}

Dati i vincoli, in ciascuna delle sezioni, ritengo che abbia senso se un po 'profondamente annidato.

Tuttavia, la mia difficoltà viene se voglio elencare ( GET) tutti i dipendenti di tutte le aziende.

Il modello di risorse per questo sarebbe più vicino a /employees(La raccolta di tutti i dipendenti)

Ciò significa che avrei dovuto /employees/{empId}anche perché, in tal caso, ci sono due URI per ottenere la stessa risorsa?

O forse l'intero schema dovrebbe essere appiattito ma ciò significherebbe che i dipendenti sono un oggetto nidificato di primo livello.

A un livello base /employees/?company={companyId}&department={deptId}restituisce esattamente la stessa visione dei dipendenti come il modello più nidificato.

Qual è la migliore pratica per i pattern URL in cui le risorse sono di proprietà di altre risorse ma dovrebbero essere interrogabili separatamente?


1
Questo è quasi esattamente il problema opposto a quello descritto in stackoverflow.com/questions/7104578/… sebbene le risposte possano essere correlate. Entrambe le domande riguardano la proprietà, ma quell'esempio implica che l'oggetto di livello superiore non è quello proprietario.
Wes,

1
Esattamente quello che mi chiedevo. Per il dato caso d'uso la tua soluzione sembra soddisfacente, ma cosa succede se la relazione è un'aggregazione anziché una composizione? Stai ancora lottando per capire quale sia la migliore pratica qui ... Inoltre, questa soluzione implica solo la creazione della relazione, ad esempio una persona esistente viene impiegata o crea un oggetto persona?
Jakob O.

Crea una persona nel mio esempio fittizio. Il motivo per cui ho usato quei termini di dominio è un esempio ragionevolmente comprensibile, sebbene imiti il ​​mio problema reale. Hai esaminato la domanda collegata che potrebbe fermarti di più per una relazione di aggregazione.
Wes,

Ho diviso la mia domanda in una risposta e una domanda.
Wes,

Risposte:


152

Quello che hai fatto è corretto. In generale, ci possono essere molti URI per la stessa risorsa - non ci sono regole che dicono che non dovresti farlo.

E in generale, potresti dover accedere agli elementi direttamente o come sottoinsieme di qualcos'altro - quindi la tua struttura ha senso per me.

Solo perché i dipendenti sono accessibili dal dipartimento:

company/{companyid}/department/{departmentid}/employees

Ciò non significa che non possano essere accessibili anche in azienda:

company/{companyid}/employees

Che restituirebbe i dipendenti per quella società. Dipende da ciò che è necessario per il tuo cliente consumato - questo è ciò per cui dovresti progettare.

Spero però che tutti i gestori di URL utilizzino lo stesso codice di supporto per soddisfare le richieste in modo da non duplicare il codice.


11
Questo sta sottolineando lo spirito di RESTful, non ci sono regole che dicono che dovresti o non dovresti fare se solo considerassi prima una risorsa significativa . Inoltre, mi chiedo quale sia la migliore pratica per non duplicare il codice in tali scenari.
abookyun

13
@abookyun se hai bisogno di entrambi i percorsi, puoi ripetere il codice del controller ripetuto tra loro per servire gli oggetti.
bgcode,

Questo non ha nulla a che fare con REST. REST non si preoccupa di come strutturi la parte del percorso dei tuoi URL ... tutto ciò che gli interessa è URI validi, si spera durevoli ...
redben

Basandomi su questa risposta, penso che qualsiasi API in cui i segmenti dinamici siano tutti identificatori univoci non dovrebbe avere bisogno di gestire più segmenti dinamici ( /company/3/department/2/employees/1). Se l'API fornisce modi per ottenere ciascuna risorsa, è possibile effettuare ciascuna di tali richieste in una libreria lato client o come endpoint unico che riutilizza il codice.
massimo

1
Sebbene non vi sia alcun divieto, ritengo più elegante avere un solo percorso verso una risorsa: semplifica tutti i modelli mentali. Preferisco anche che gli URI non cambino il loro tipo di risorsa in caso di nidificazione. ad esempio, /company/*dovrebbe restituire solo la risorsa aziendale e non modificare affatto il tipo di risorsa. Niente di tutto ciò è specificato da REST - è generalmente una specifica poco definita - solo una preferenza personale.
Kashif,

174

Ho provato entrambe le strategie di progettazione: endpoint nidificati e non nidificati. Ho scoperto che:

  1. se la risorsa nidificata ha una chiave primaria e non si dispone della chiave primaria principale, la struttura nidificata richiede di ottenerla, anche se il sistema in realtà non la richiede.

  2. gli endpoint nidificati in genere richiedono endpoint ridondanti. In altre parole, il più delle volte sarà necessario l'endpoint aggiuntivo / dipendenti in modo da poter ottenere un elenco di dipendenti tra i reparti. Se hai / dipendenti, cosa ti acquistano esattamente / aziende / dipartimenti / dipendenti?

  3. gli endpoint di nidificazione non si evolvono altrettanto bene. Ad esempio, potresti non aver bisogno di cercare dipendenti ora, ma potresti in seguito e se hai una struttura nidificata, non hai altra scelta che aggiungere un altro endpoint. Con un design non nidificato, aggiungi solo più parametri, il che è più semplice.

  4. a volte una risorsa può avere più tipi di genitori. Il risultato in più endpoint restituisce tutti la stessa risorsa.

  5. endpoint ridondanti rendono i documenti più difficili da scrivere e rendono anche l'api più difficile da imparare.

In breve, il design non nidificato sembra consentire uno schema di endpoint più flessibile e più semplice.


24
È stato molto rinfrescante trovare questa risposta. Uso diversi endpoint nidificati ormai da diversi mesi dopo che mi è stato insegnato che era "la strada giusta". Sono arrivato alle stesse conclusioni che hai elencato sopra. Molto più semplice con un design non nidificato.
user3344977

6
Sembri elencare alcuni degli aspetti negativi come aspetti positivi. "Basta inserire più parametri in un singolo end-point" rende l'API più difficile da documentare e apprendere, non viceversa. ;-)
Drenmi il

4
Non è un fan di questa risposta. Non è necessario introdurre endpoint ridondanti solo perché è stata aggiunta una risorsa nidificata. Inoltre, non è un problema avere la stessa risorsa restituita da più genitori, a condizione che quei genitori possiedano effettivamente la risorsa nidificata. Non è un problema ottenere una risorsa padre per imparare come interagire con le risorse nidificate. Una buona API REST rilevabile dovrebbe fare questo.
Scottm,

3
@Scottm - Uno svantaggio delle risorse nidificate che ho riscontrato è che potrebbe portare alla restituzione di dati errati se gli ID delle risorse padre sono errati / non corrispondenti. Supponendo che non vi siano problemi di autorizzazione, viene lasciato all'implementazione API per verificare che la risorsa nidificata sia effettivamente figlia della risorsa padre che viene passata. Se questo controllo non è codificato, la risposta api potrebbe essere errata e portare alla corruzione. Quali sono i tuoi pensieri?
Andy Dufresne,

1
Non sono necessari gli ID parent intermedi se le risorse finali hanno tutti ID univoci. Ad esempio, per ottenere il dipendente tramite ID hai GET / aziende / dipartimenti / dipendenti / {empId} o per ottenere tutti i dipendenti dell'azienda 123 hai GET / aziende / 123 / dipartimenti / dipendenti / Mantenere il percorso gerarchico rende più chiaro come puoi arrivare alle risorse intermedie per filtrare / creare / modificare e aiuta con la rilevabilità secondo me.
PaulG

77

Ho spostato ciò che ho fatto dalla domanda a una risposta in cui è probabile che più persone la vedano.

Quello che ho fatto è avere gli endpoint di creazione nell'endpoint nidificato. L'endpoint canonico per la modifica o l'interrogazione di un elemento non si trova nella risorsa nidificata .

Quindi in questo esempio (elencando solo gli endpoint che cambiano una risorsa)

  • POST /companies/ crea una nuova società restituisce un collegamento alla società creata.
  • POST /companies/{companyId}/departments quando viene creato un dipartimento, il nuovo dipartimento restituisce un collegamento a /departments/{departmentId}
  • PUT /departments/{departmentId} modifica un dipartimento
  • POST /departments/{deparmentId}/employees crea un nuovo dipendente a cui restituisce un collegamento /employees/{employeeId}

Quindi ci sono risorse a livello di radice per ciascuna delle raccolte. Tuttavia, la creazione è nell'oggetto proprietario .


4
Ho escogitato anche lo stesso tipo di design. Penso che sia intuitivo creare cose come queste "a cui appartengono", ma poter comunque elencarle a livello globale. Ancor di più quando c'è una relazione in cui una risorsa DEVE avere un genitore. Quindi creare quella risorsa a livello globale non è così ovvio, ma farlo in una sub-risorsa come questa ha perfettamente senso.
Joakim,

Immagino tu abbia usato il POSTsignificato PUT, e altrimenti.
Gerardo Lima,

In realtà no Nota che non sto usando ID assegnati in precedenza per la creazione poiché il server in questo caso è responsabile della restituzione dell'ID (nel collegamento). Pertanto la scrittura di POST è corretta (non è possibile ottenere la stessa implementazione). Il put cambia comunque l'intera risorsa ma è ancora disponibile nella stessa posizione, quindi l'ho messo. PUT vs POST è una questione diversa ed è anche controversa. Ad esempio stackoverflow.com/questions/630453/put-vs-post-in-rest
Wes,

@Wes Anche io preferisco modificare i metodi verbali per essere sotto il genitore. Ma vedi che il passaggio del parametro di query per la risorsa globale è accettato bene? Es .: POST / dipartimenti con parametro query company = ID azienda
Ayyappa

1
@Mohamad Se pensi che l'altro modo sia più semplice sia nella comprensione che nell'applicazione dei vincoli, sentiti libero di dare una risposta. Si tratta di rendere esplicita la mappatura in questo caso. Potrebbe funzionare con un parametro ma è proprio questa la domanda. Qual è il modo migliore.
Wes,

35

Ho letto tutte le risposte di cui sopra ma sembra che non abbiano una strategia comune. Ho trovato un buon articolo sulle migliori pratiche in Design API da Microsoft Documents . Penso che dovresti fare riferimento.

In sistemi più complessi, può essere allettante fornire URI che consentono a un client di navigare attraverso diversi livelli di relazioni, come /customers/1/orders/99/products. Tuttavia, questo livello di complessità può essere difficile da mantenere ed è poco flessibile se le relazioni tra le risorse cambiano in futuro. Invece, cerca di mantenere gli URI relativamente semplici . Una volta che un'applicazione ha un riferimento a una risorsa, dovrebbe essere possibile utilizzare questo riferimento per trovare elementi correlati a quella risorsa. La query precedente può essere sostituita con l'URI /customers/1/ordersper trovare tutti gli ordini per il cliente 1 e quindi /orders/99/productsper trovare i prodotti in questo ordine.

.

Mancia

Evitare di richiedere URI delle risorse più complessi di collection/item/collection.


3
Il riferimento che dai è sorprendente, insieme al punto in cui speri di non creare URI complessi.
vicco,

Quindi, quando voglio creare un team per un utente, dovrebbe essere POST / team (userId in thebody) o POST / users /: id / teams
coinhndp

@coinhndp Ciao, dovresti usare POST / team e potresti ottenere userId dopo aver autorizzato il token di accesso. Voglio dire, quando crei un materiale hai bisogno di un codice di autorizzazione, giusto? Non so quale framework stai usando, ma sono sicuro che potresti ottenere userId nel controller API. Ad esempio: nell'API ASP.NET, chiamare RequestContext.Principal dall'interno di un metodo su ApiController. In Spring Secirity, SecurityContextHolder.getContext (). GetAuthentication (). GetPrincipal () ti aiuterà. In AWS NodeJS Lambda, ovvero cognito: nome utente nell'oggetto header.
Long Nguyen,

Quindi cosa c'è che non va nel POST / users /: id / teams. Penso che sia raccomandato nel documento Microsoft che hai pubblicato sopra
coinhndp il

@coinhndp Se crei un team come amministratore, va bene. Ma, come normali utenti, non so perché hai bisogno di userId nel percorso? Suppongo che abbiamo user_A e user_B, cosa pensi se user_A potrebbe creare un nuovo team per user_B se user_A chiama POST / users / user_B / teams. Pertanto, in questo caso non è necessario passare userId, userId potrebbe ottenere dopo l'autorizzazione. Ma team /: id / projects è buono per fare una relazione tra team e progetto, ad esempio.
Long Nguyen,

10

L'aspetto dei tuoi URL non ha nulla a che fare con REST. Tutto va bene. In realtà è un "dettaglio di implementazione". Quindi, proprio come il nome delle variabili. Tutto ciò che devono essere è unico e durevole.

Non perdere troppo tempo su questo, basta fare una scelta e attenersi ad essa / essere coerenti. Ad esempio, se vai con le gerarchie, lo fai per tutte le tue risorse. Se si utilizzano parametri di query ... ecc. Proprio come le convenzioni di denominazione nel codice.

Perchè così ? Per quanto ne so, un'API "RESTful" deve essere sfogliabile (sai ... "Hypermedia come motore dello stato dell'applicazione"), quindi un client API non si preoccupa di come sono i tuoi URL purché siano valido (non c'è SEO, nessun essere umano che deve leggere quegli "URL amichevoli", tranne che per il debug ...)

Quanto è bello / comprensibile un URL in un'API REST è interessante solo per te come sviluppatore API, non come client API, come sarebbe il nome di una variabile nel tuo codice.

La cosa più importante è che il tuo client API sappia come interpretare il tuo tipo di media. Ad esempio, sa che:

  • il tuo tipo di media ha una proprietà di collegamenti che elenca i collegamenti disponibili / correlati.
  • Ogni collegamento è identificato da una relazione (proprio come i browser sanno che il collegamento [rel = "foglio di stile"] significa che è un foglio di stile o rel = favico è un collegamento a una favicon ...)
  • e sa cosa significano quelle relazioni ("aziende" significa un elenco di aziende, "ricerca" significa un URL basato su modelli per fare una ricerca in un elenco di risorse, "dipartimenti" significa dipartimenti della risorsa corrente)

Di seguito è riportato un esempio di scambio HTTP (i corpi sono in yaml poiché è più facile da scrivere):

Richiesta

GET / HTTP/1.1
Host: api.acme.io
Accept: text/yaml, text/acme-mediatype+yaml

Risposta: un elenco di collegamenti alle risorse principali (aziende, persone, qualunque cosa ...)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:04:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: this is your API's entrypoint (like a homepage)  
links:
  # could be some random path https://api.acme.local/modskmklmkdsml
  # the only thing the API client cares about is the key (or rel) "companies"
  companies: https://api.acme.local/companies
  people: https://api.acme.local/people

Richiesta: collegamento a società (utilizzando body.links.companies della risposta precedente)

GET /companies HTTP/1.1
Host: api.acme.local
Accept: text/yaml, text/acme-mediatype+yaml

Risposta: un elenco parziale di aziende (sotto gli articoli), la risorsa contiene collegamenti correlati, come il collegamento per ottenere la coppia successiva di società (body.links.next) un altro collegamento (basato su modelli) per la ricerca (body.links.search)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:06:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: representation of a list of companies
links:
  # link to the next page
  next: https://api.acme.local/companies?page=2
  # templated link for search
  search: https://api.acme.local/companies?query={query} 
# you could provide available actions related to this resource
actions:
  add:
    href: https://api.acme.local/companies
    method: POST
items:
  - name: company1
    links:
      self: https://api.acme.local/companies/8er13eo
      # and here is the link to departments
      # again the client only cares about the key department
      department: https://api.acme.local/companies/8er13eo/departments
  - name: company2
    links:
      self: https://api.acme.local/companies/9r13d4l
      # or could be in some other location ! 
      department: https://api2.acme.local/departments?company=8er13eo

Quindi, come vedi se segui i collegamenti / le relazioni nel modo in cui strutturi la parte del percorso dei tuoi URL non ha alcun valore per il tuo client API. E se stai comunicando la struttura dei tuoi URL al tuo cliente come documentazione, allora non stai eseguendo REST (o almeno non il Livello 3 secondo " Modello di maturità di Richardson ")


7
"Quanto è bello / comprensibile un URL in un'API REST è interessante solo per te come sviluppatore API, non come client API, come sarebbe il nome di una variabile nel tuo codice." Perché questo NON dovrebbe essere interessante? Questo è molto importante, se qualcuno, tranne te stesso, utilizza anche l'API. Questo fa parte dell'esperienza utente, quindi direi che è molto importante che sia facile da capire per gli sviluppatori di client API. Rendere le cose ancora più facili da capire collegando chiaramente le risorse è ovviamente un bonus (livello 3 nell'URL fornito). Tutto dovrebbe essere intuitivo e logico con relazioni chiare.
Joakim,

1
@Joakim Se stai creando un'API di riposo di livello 3 (Hypertext As The Engine Of Application State), la struttura del percorso dell'URL non ha assolutamente alcun interesse per il client (purché sia ​​valida). Se non stai mirando al livello 3, allora sì, è importante e dovrebbe essere ipotizzabile. Ma il vero REST è di livello 3. Un buon articolo: martinfowler.com/articles/richardsonMaturityModel.html
redben

4
Mi oppongo alla creazione di un'API o dell'interfaccia utente che non sia di facile utilizzo per gli esseri umani. Livello 3 o no, sono d'accordo che collegare le risorse sia un'ottima idea. Ma suggerire di farlo "rende possibile cambiare lo schema degli URL" significa essere fuori contatto con la realtà e come le persone usano le API. Quindi è una cattiva raccomandazione. Ma sicuramente nel migliore dei mondi tutti sarebbero al REST di livello 3. Incorpora collegamenti ipertestuali E utilizzo uno schema URL comprensibile umanamente. Il livello 3 non esclude il primo, e secondo me DOVREBBE preoccuparsene. Buon articolo però :)
Joakim,

Si dovrebbe ovviamente preoccuparsi del bene della manutenibilità e di altre preoccupazioni, penso che ti sfugga il punto della mia risposta: il modo in cui l'URL sembra non merita molto pensiero e dovresti "solo fare una scelta e attenersi ad essa / essere coerente ", come ho detto nella risposta. E nel caso di un'API REST, almeno la mia opinione, la facilità d'uso non è nell'URL, è principalmente nel (tipo di media) Comunque spero che tu capisca il mio punto :)
redben

9

Non sono d'accordo con questo tipo di percorso

GET /companies/{companyId}/departments

Se vuoi ottenere dipartimenti, penso che sia meglio usare una risorsa / dipartimenti

GET /departments?companyId=123

Suppongo che tu abbia una companiestabella e una departmentstabella, quindi le classi per mapparle nel linguaggio di programmazione che usi. Suppongo anche che i dipartimenti possano essere collegati ad entità diverse dalle aziende, quindi una risorsa / dipartimenti è semplice, è conveniente avere risorse mappate alle tabelle e inoltre non sono necessari tanti endpoint poiché è possibile riutilizzare

GET /departments?companyId=123

per qualsiasi tipo di ricerca, ad esempio

GET /departments?name=xxx
GET /departments?companyId=123&name=xxx
etc.

Se si desidera creare un dipartimento, il

POST /departments

la risorsa deve essere utilizzata e l'organismo di richiesta deve contenere l'ID azienda (se il reparto può essere collegato a una sola società).


1
Per me, questo è un approccio accettabile solo se l'oggetto nidificato ha senso come oggetto atomico. Se non lo sono, non avrebbe davvero senso separarli.
Simme,

Questo è quello che ho detto, se si desidera anche essere in grado di recuperare i reparti, ovvero se si utilizzerà un endpoint / dipartimenti.
Maxime Laval,

2
Può anche avere senso consentire l'inclusione dei reparti tramite caricamento lento durante il recupero di un'azienda, ad esempio GET /companies/{companyId}?include=departments, poiché ciò consente sia alla società che ai suoi reparti di essere recuperati in una singola richiesta HTTP. Fractal lo fa davvero bene.
Matthew Daly,

1
Quando stai configurando acls, probabilmente vuoi limitare l' /departmentsendpoint in modo che sia accessibile solo da un amministratore e ogni azienda acceda ai propri reparti solo tramite `/ companies / {companyId} / dipartimenti`
Cuzox,

@MatthewDaly OData lo fa bene anche con $ expand
Rob Grant il

1

Rails offre una soluzione a questo: annidamento superficiale .

Penso che questo sia positivo perché quando si ha a che fare direttamente con una risorsa nota, non è necessario utilizzare percorsi nidificati, come è stato discusso in altre risposte qui.

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.