Come modellare un'API RESTful con ereditarietà?


88

Ho una gerarchia di oggetti che devo esporre tramite un'API RESTful e non sono sicuro di come dovrebbero essere strutturati i miei URL e cosa dovrebbero restituire. Non sono riuscito a trovare best practice.

Diciamo che ho cani e gatti che ereditano da animali. Ho bisogno di operazioni CRUD su cani e gatti; Voglio anche poter fare operazioni sugli animali in generale.

La mia prima idea era di fare qualcosa del genere:

GET /animals        # get all animals
POST /animals       # create a dog or cat
GET /animals/123    # get animal 123

Il fatto è che la collezione / animals ora è "incoerente", in quanto può tornare e prendere oggetti che non hanno esattamente la stessa struttura (cani e gatti). È considerato "RESTful" avere una raccolta che restituisce oggetti con attributi diversi?

Un'altra soluzione sarebbe creare un URL per ogni tipo concreto, come questo:

GET /dogs       # get all dogs
POST /dogs      # create a dog
GET /dogs/123   # get dog 123

GET /cats       # get all cats
POST /cats      # create a cat
GET /cats/123   # get cat 123

Ma ora il rapporto tra cani e gatti è perso. Se si desidera recuperare tutti gli animali, è necessario interrogare le risorse del cane e del gatto. Anche il numero di URL aumenterà con ogni nuovo sottotipo di animale.

Un altro suggerimento è stato quello di aumentare la seconda soluzione aggiungendo questo:

GET /animals    # get common attributes of all animals

In questo caso, gli animali restituiti conterrebbero solo attributi comuni a tutti gli animali, eliminando attributi specifici del cane e del gatto. Ciò consente di recuperare tutti gli animali, anche se con meno dettagli. Ogni oggetto restituito potrebbe contenere un collegamento alla versione dettagliata e concreta.

Eventuali commenti o suggerimenti?

Risposte:


41

Io suggerirei:

  • Utilizzo di un solo URI per risorsa
  • Differenziare tra animali esclusivamente a livello di attributo

L'impostazione di più URI per la stessa risorsa non è mai una buona idea perché può causare confusione ed effetti collaterali imprevisti. Detto questo, il tuo singolo URI dovrebbe essere basato su uno schema generico come /animals.

La prossima sfida di affrontare l'intera collezione di cani e gatti a livello "base" è già risolta in virtù dell'approccio /animalsURI.

La sfida finale di trattare tipi specializzati come cani e gatti può essere facilmente risolta utilizzando una combinazione di parametri di query e attributi di identificazione all'interno del tipo di supporto. Per esempio:

GET /animals( Accept : application/vnd.vet-services.animals+json)

{
   "animals":[
      {
         "link":"/animals/3424",
         "type":"dog",
         "name":"Rex"
      },
      {
         "link":"/animals/7829",
         "type":"cat",
         "name":"Mittens"
      }
   ]
}
  • GET /animals - ottiene tutti i cani e gatti, restituirebbe sia Rex che Mittens
  • GET /animals?type=dog - prende tutti i cani, tornerebbe solo Rex
  • GET /animals?type=cat - ottiene tutti i gatti, sarebbe solo Mittens

Quindi, durante la creazione o la modifica degli animali, spetterebbe al chiamante specificare il tipo di animale coinvolto:

Tipo di supporto: application/vnd.vet-services.animal+json

{
   "type":"dog",
   "name":"Fido"
}

Il payload di cui sopra potrebbe essere inviato con una richiesta POSTo PUT.

Lo schema di cui sopra ti fornisce le caratteristiche di base simili all'ereditarietà OO tramite REST e con la possibilità di aggiungere ulteriori specializzazioni (cioè più tipi di animali) senza interventi chirurgici importanti o modifiche al tuo schema URI.


Sembra molto simile al "casting" tramite un'API REST. Mi ricorda anche i problemi / soluzioni nel layout di memoria di una sottoclasse C ++. Ad esempio dove e come rappresentare contemporaneamente una base e una sottoclasse con un unico indirizzo in memoria.
trcarden

10
Suggerisco: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
dipold

1
Oltre a specificare il tipo desiderato come parametro di richiesta GET: mi sembra che potresti usare il tipo di accettazione per ottenere anche questo. Cioè: GET /animals Accettaapplication/vnd.vet-services.animal.dog+json
BrianT.

22
E se cane e gatto hanno ciascuno proprietà uniche? Come lo gestireste durante il POSTfunzionamento, poiché la maggior parte dei framework non saprebbe come deserializzarlo correttamente in un modello poiché json non trasporta buone informazioni di digitazione. Come gestireste i casi postali, ad esempio [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}?
LB2

1
E se cani e gatti avessero (la maggioranza) proprietà diverse? es. # 1 POST di una comunicazione per SMS (a, maschera) rispetto a un'e-mail (indirizzo e-mail, cc, bcc, to, from, isHtml), o eg # 2 POST di una FundingSource per CreditCard (maskedPan, nameOnCard, Expiry) vs. a BankAccount (bsb, accountNumber) ... useresti comunque una singola risorsa API? Ciò sembrerebbe violare la responsabilità unica dei principi SOLID, ma non sono sicuro che ciò si applichi alla progettazione API ...
Ryan.Bartsch

5

Questa domanda può essere risolta meglio con il supporto di un recente miglioramento introdotto nell'ultima versione di OpenAPI.

È stato possibile combinare schemi utilizzando parole chiave come oneOf, allOf, anyOf e ottenere un payload del messaggio convalidato a partire dallo schema JSON v1.0.

https://spacetelescope.github.io/understanding-json-schema/reference/combining.html

Tuttavia, in OpenAPI (ex Swagger), la composizione degli schemi è stata migliorata dalle parole chiave discriminator (v2.0 +) e oneOf (v3.0 +) per supportare veramente il polimorfismo.

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaComposition

La tua eredità potrebbe essere modellata usando una combinazione di oneOf (per scegliere uno dei sottotipi) e allOf (per combinare il tipo e uno dei suoi sottotipi). Di seguito è una definizione di esempio per il metodo POST.

paths:
  /animals:
    post:
      requestBody:
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/Dog'
              - $ref: '#/components/schemas/Cat'
              - $ref: '#/components/schemas/Fish'
            discriminator:
              propertyName: animal_type
     responses:
       '201':
         description: Created

components:
  schemas:
    Animal:
      type: object
      required:
        - animal_type
        - name
      properties:
        animal_type:
          type: string
        name:
          type: string
      discriminator:
        property_name: animal_type
    Dog:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            playsFetch:
              type: string
    Cat:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            likesToPurr:
              type: string
    Fish:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            water-type:
              type: string

1
È vero che l'OAS lo consente. Tuttavia, non esiste alcun supporto per la visualizzazione della funzione nell'interfaccia utente di Swagger ( collegamento ) e penso che una funzione sia di uso limitato se non puoi mostrarla a nessuno.
emft

1
@emft, non vero. Al momento della stesura di questa risposta, l'interfaccia utente di Swagger lo supporta già.
Andrejs Cainikovs

Grazie, funziona benissimo! Al momento sembra vero che l'interfaccia utente di Swagger non lo mostri completamente. I modelli verranno visualizzati nella sezione Schemi in basso e qualsiasi sezione di risposte che fa riferimento alla sezione oneOf verrà parzialmente visualizzata nell'interfaccia utente (solo schema, nessun esempio), ma non si ottiene alcun corpo di esempio per l'input della richiesta. Il problema di GitHub per questo è stato aperto per 3 anni, quindi è probabile che rimanga tale: github.com/swagger-api/swagger-ui/issues/3803
Jethro

4

Vorrei scegliere / animals restituendo un elenco di cani e pesci e quant'altro:

<animals>
  <animal type="dog">
    <name>Fido</name>
    <fur-color>White</fur-color>
  </animal>
  <animal type="fish">
    <name>Wanda</name>
    <water-type>Salt</water-type>
  </animal>
</animals>

Dovrebbe essere facile implementare un esempio JSON simile.

I clienti possono sempre fare affidamento sulla presenza dell'elemento "nome" (un attributo comune). Ma a seconda dell'attributo "tipo" ci saranno altri elementi come parte della rappresentazione animale.

Non c'è nulla di intrinsecamente RESTful o unRESTful nel restituire un elenco di questo tipo - REST non prescrive alcun formato specifico per rappresentare i dati. Tutto ciò che dice è che i dati devono avere una rappresentazione e il formato per quella rappresentazione è identificato dal tipo di media (che in HTTP è l'intestazione Content-Type).

Pensa ai tuoi casi d'uso: devi mostrare un elenco di animali misti? Bene, quindi restituisci un elenco di dati sugli animali misti. Hai bisogno solo di un elenco di cani? Bene, fai una lista del genere.

Che tu faccia / animals? Type = dog o / dogs è irrilevante rispetto a REST che non prescrive alcun formato URL, che viene lasciato come dettaglio di implementazione al di fuori dell'ambito di REST. REST afferma solo che le risorse dovrebbero avere identificatori, non importa quale formato.

È necessario aggiungere alcuni collegamenti ipermediali per avvicinarsi a un'API RESTful. Ad esempio aggiungendo riferimenti ai dettagli degli animali:

<animals>
  <animal type="dog" href="https://stackoverflow.com/animals/123">
    <name>Fido</name>
    <fur-color>White</fur-color>
  </animal>
  <animal type="fish" href="https://stackoverflow.com/animals/321">
    <name>Wanda</name>
    <water-type>Salt</water-type>
  </animal>
</animals>

Aggiungendo il collegamento ipermediale si riduce l'accoppiamento client / server: nel caso precedente si elimina il peso della costruzione dell'URL dal client e si lascia che il server decida come costruire gli URL (di cui per definizione è l'unica autorità).


1

Ma ora il rapporto tra cani e gatti è perso.

Effettivamente, ma tieni presente che l'URI semplicemente non riflette mai le relazioni tra gli oggetti.


1

So che questa è una vecchia domanda, ma sono interessato a indagare su ulteriori problemi su un modello di ereditarietà REST

Posso sempre dire che un cane è un animale e anche una gallina, ma la gallina fa le uova mentre il cane è un mammifero, quindi non può. Un'API come

OTTIENI animali /: animalID / uova

non è coerente perché indica che tutti i sottotipi di animali possono avere uova (come conseguenza della sostituzione di Liskov). Ci sarebbe un fallback se tutti i mammiferi rispondessero con "0" a questa richiesta, ma cosa succede se abilito anche un metodo POST? Devo aver paura che domani ci siano uova di cane nelle mie crepes?

L'unico modo per gestire questi scenari è fornire una 'super-risorsa' che aggreghi tutte le sottorisorse condivise tra tutte le possibili 'risorse-derivate' e quindi una specializzazione per ogni risorsa-derivata che ne ha bisogno, proprio come quando abbattiamo un oggetto in oop

GET / animali /: animalID / figli GET / galline /: animalID / uova POST / galline /: animalID / uova

Lo svantaggio, qui, è che qualcuno potrebbe passare un ID cane per fare riferimento a un'istanza di raccolta di galline, ma il cane non è una gallina, quindi non sarebbe errato se la risposta fosse 404 o 400 con un messaggio di motivazione

Ho sbagliato?


1
Penso che tu stia dando troppa enfasi alla struttura dell'URI. L'unico modo per accedere a "animals /: animalID / eggs" è tramite HATEOAS. Quindi dovresti prima richiedere l'animale tramite "animals /: animalID" e poi per quegli animali che possono avere uova, ci sarà un collegamento a "animals /: animalID / eggs", e per quelli che non lo fanno, non ci sarà essere un collegamento per passare dall'animale alle uova. Se qualcuno in qualche modo finisce alle uova per un animale che non può avere uova, restituisci il codice di stato HTTP appropriato (non trovato o vietato per esempio)
wired_in

0

Sì, ti sbagli. Anche le relazioni possono essere modellate seguendo le specifiche OpenAPI, ad esempio in questo modo polimorfico.

Chicken:
  type: object
  discriminator:
    propertyName: typeInformation
  allOf:
    - $ref:'#components/schemas/Chicken'
    - type: object
      properties:
        eggs:
          type: array
          items:
            $ref:'#/components/schemas/Egg'
          name:
            type: string

...


Commento aggiuntivo: la messa a fuoco del percorso API GET chicken/eggs dovrebbe funzionare anche utilizzando i generatori di codice OpenAPI comuni per i controller, ma non l'ho ancora verificato. Forse qualcuno può provare?
Andreas Gaus
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.