Wie modelliere ich eine RESTful-API mit Vererbung?

85

Ich habe eine Objekthierarchie, die ich über eine RESTful-API verfügbar machen muss, und ich bin nicht sicher, wie meine URLs strukturiert sein sollen und was sie zurückgeben sollen. Ich konnte keine Best Practices finden.

Nehmen wir an, ich habe Hunde und Katzen, die von Tieren erben. Ich brauche CRUD-Operationen an Hunden und Katzen. Ich möchte auch in der Lage sein, Operationen an Tieren im Allgemeinen durchzuführen.

Meine erste Idee war, so etwas zu machen:

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

Die Sache ist, dass die / animal-Sammlung jetzt "inkonsistent" ist, da sie zurückkehren und Objekte aufnehmen kann, die nicht genau die gleiche Struktur haben (Hunde und Katzen). Wird es als "RESTful" angesehen, wenn eine Sammlung Objekte mit unterschiedlichen Attributen zurückgibt?

Eine andere Lösung wäre, eine URL für jeden konkreten Typ wie folgt zu erstellen:

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

Aber jetzt ist die Beziehung zwischen Hunden und Katzen verloren. Wenn alle Tiere abgerufen werden sollen, müssen sowohl die Hunde- als auch die Katzenressourcen abgefragt werden. Die Anzahl der URLs erhöht sich auch mit jedem neuen Tier-Subtyp.

Ein weiterer Vorschlag war, die zweite Lösung durch Hinzufügen zu ergänzen:

GET /animals    # get common attributes of all animals

In diesem Fall würden die zurückgegebenen Tiere nur Attribute enthalten, die allen Tieren gemeinsam sind, wobei hundespezifische und katzenspezifische Attribute gelöscht würden. Dies ermöglicht das Abrufen aller Tiere, wenn auch mit weniger Details. Jedes zurückgegebene Objekt kann einen Link zur detaillierten, konkreten Version enthalten.

Irgendwelche Kommentare oder Vorschläge?

Alpha Hydrae
quelle

Antworten:

39

Ich würde vorschlagen:

  • Verwenden nur eines URI pro Ressource
  • Unterscheidung zwischen Tieren ausschließlich auf Attributebene

Das Einrichten mehrerer URIs für dieselbe Ressource ist niemals eine gute Idee, da dies zu Verwirrung und unerwarteten Nebenwirkungen führen kann. Vor diesem Hintergrund sollte Ihre einzelne URI auf einem generischen Schema wie basieren /animals.

Die nächste Herausforderung, sich mit der gesamten Sammlung von Hunden und Katzen auf der "Basisebene" zu befassen, ist bereits durch den /animalsURI-Ansatz gelöst .

Die letzte Herausforderung beim Umgang mit speziellen Typen wie Hunden und Katzen kann mithilfe einer Kombination aus Abfrageparametern und Identifikationsattributen innerhalb Ihres Medientyps leicht gelöst werden. Beispielsweise:

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 - bekommt alle Hunde und Katzen, würde sowohl Rex als auch Mittens zurückgeben
  • GET /animals?type=dog - bekommt alle Hunde, würde nur Rex zurückgeben
  • GET /animals?type=cat - bekommt alle Katzen, würde nur Fäustlinge

Beim Erstellen oder Ändern von Tieren muss der Anrufer dann die Art des betroffenen Tieres angeben:

Medientyp: application/vnd.vet-services.animal+json

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

Die oben genannten Nutzdaten können mit einer POSToder PUTAnfrage gesendet werden .

Mit dem obigen Schema erhalten Sie die grundlegenden ähnlichen Merkmale wie bei der OO-Vererbung durch REST und können weitere Spezialisierungen (dh mehr Tierarten) hinzufügen, ohne dass eine größere Operation oder Änderungen an Ihrem URI-Schema erforderlich sind.

Brian Kelly
quelle
Dies scheint dem "Casting" über eine REST-API sehr ähnlich zu sein. Es erinnert mich auch an die Probleme / Lösungen im Speicherlayout einer C ++ - Unterklasse. Zum Beispiel, wo und wie beide gleichzeitig eine Basis und eine Unterklasse mit einer einzelnen Adresse im Speicher darstellen.
Trcarden
10
Ich schlage vor: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
Dipold
1
Zusätzlich zur Angabe des gewünschten Typs als GET-Anforderungsparameter: Meines Erachtens könnten Sie auch den Typ accept verwenden, um dies zu erreichen. Das heißt: GET /animals Akzeptierenapplication/vnd.vet-services.animal.dog+json
BrianT.
19
Was ist, wenn Katze und Hund jeweils einzigartige Eigenschaften haben? Wie würden Sie damit im POSTBetrieb umgehen , da die meisten Frameworks nicht wissen, wie sie es ordnungsgemäß in ein Modell deserialisieren können, da json keine guten Tippinformationen enthält. Wie würden Sie zB mit Post-Fällen umgehen [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}?
LB2
@ LB2 Dies ist eine Einschränkung einiger Frameworks um 120%. Ich denke, in diesem Fall müssten Sie ein Pseudo "Modell" definieren, das definiert ist, um alle Ihre möglichen POST-Parameter zu akzeptieren. Führen Sie dann eine zusätzliche Validierung für den tatsächlichen Objekttyp durch, dh die Prüfung, dogs.likesToPurrdie nicht definiert ist. In einem lose strukturierten Framework wie Flask können Sie typezuerst das Attribut überprüfen und dann das Objekt je nach Typ zusammenstellen.
mdw7326
4

Ich würde für / Tiere gehen und eine Liste von Hunden und Fischen zurückgeben und was auch immer:

<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>

Es sollte einfach sein, ein ähnliches JSON-Beispiel zu implementieren.

Clients können sich immer darauf verlassen, dass das Element "name" vorhanden ist (ein gemeinsames Attribut). Abhängig vom Attribut "Typ" gibt es jedoch andere Elemente als Teil der Tierdarstellung.

Es gibt nichts von Natur aus RESTful oder unRESTful bei der Rückgabe einer solchen Liste - REST schreibt kein bestimmtes Format für die Darstellung von Daten vor. Es sagt nur, dass Daten müssen einig Darstellung und das Format für die Darstellung durch den Medientyp (die in HTTP ist der Content-Type - Header) identifiziert.

Denken Sie über Ihre Anwendungsfälle nach - müssen Sie eine Liste gemischter Tiere anzeigen? Dann geben Sie eine Liste mit gemischten Tierdaten zurück. Benötigen Sie nur eine Liste von Hunden? Nun, machen Sie eine solche Liste.

Ob Sie / Tiere? Typ = Hund oder / Hunde tun, ist in Bezug auf REST irrelevant, das keine URL-Formate vorschreibt - dies bleibt als Implementierungsdetail außerhalb des Bereichs von REST. REST gibt nur an, dass Ressourcen Bezeichner haben sollten - egal welches Format.

Sie sollten einige Hyper-Media-Links hinzufügen, um einer RESTful-API näher zu kommen. Zum Beispiel durch Hinzufügen von Verweisen auf die Tierdetails:

<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>

Durch Hinzufügen von Hyper-Media-Links reduzieren Sie die Client / Server-Kopplung. In diesem Fall entlasten Sie den Client von der URL-Erstellung und lassen den Server entscheiden, wie URLs erstellt werden sollen (von denen er per Definition die einzige Autorität ist).

Jørn Wildt
quelle
3

Diese Frage kann mit der Unterstützung einer kürzlich in der neuesten Version von OpenAPI eingeführten Erweiterung besser beantwortet werden.

Es war möglich, Schemas mit Schlüsselwörtern wie oneOf, allOf, anyOf zu kombinieren und eine seit JSON-Schema v1.0 validierte Nachrichtennutzlast zu erhalten.

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

In OpenAPI (ehemals Swagger) wurde die Schemakomposition jedoch durch die Schlüsselwörter Diskriminator (v2.0 +) und oneOf (v3.0 +) verbessert, um den Polymorphismus wirklich zu unterstützen.

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

Ihre Vererbung kann mithilfe einer Kombination aus oneOf (zur Auswahl eines der Subtypen) und allOf (zur Kombination des Typs und eines seiner Subtypen) modelliert werden. Unten finden Sie eine Beispieldefinition für die POST-Methode.

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
dbaltor
quelle
Es ist wahr, dass OAS dies zulässt. Es gibt jedoch keine Unterstützung für die Anzeige der Funktion in der Swagger-Benutzeroberfläche ( Link ), und ich denke, eine Funktion ist von begrenztem Nutzen, wenn Sie sie niemandem zeigen können.
emft
@ Emft, nicht wahr. Zum Zeitpunkt des Schreibens dieser Antwort unterstützt Swagger UI dies bereits.
Andrejs Cainikovs
1

Aber jetzt ist die Beziehung zwischen Hunden und Katzen verloren.

Beachten Sie jedoch, dass URI niemals die Beziehungen zwischen Objekten widerspiegelt.

G. Demecki
quelle
1

Ich weiß, dass dies eine alte Frage ist, aber ich bin daran interessiert, weitere Probleme bei einer RESTful-Vererbungsmodellierung zu untersuchen

Ich kann immer sagen, dass ein Hund ein Tier und auch eine Henne ist, aber eine Henne macht Eier, während ein Hund ein Säugetier ist, also kann es nicht. Eine API wie

GET Tiere /: animalID / Eier

ist nicht konsistent, da dies darauf hinweist, dass alle Subtypen von Tieren Eier haben können (als Folge der Liskov-Substitution). Es würde einen Fallback geben, wenn alle Säugetiere mit '0' auf diese Anfrage antworten, aber was ist, wenn ich auch eine POST-Methode aktiviere? Sollte ich befürchten, dass morgen Hundeeier in meinen Crepes sein würden?

Die einzige Möglichkeit, mit diesen Szenarien umzugehen, besteht darin, eine "Superressource" bereitzustellen, die alle Teilressourcen zusammenfasst, die von allen möglichen "abgeleiteten Ressourcen" gemeinsam genutzt werden, und dann eine Spezialisierung für jede abgeleitete Ressource, die sie benötigt, genau wie beim Downcasting eines Objekts in oop

GET / Tiere /: animalID / Söhne GET / Hühner /: animalID / Eier POST / Hühner /: animalID / Eier

Der Nachteil hierbei ist, dass jemand eine Hunde-ID übergeben könnte, um auf eine Instanz der Hühnersammlung zu verweisen, aber der Hund ist keine Henne, daher wäre es nicht falsch, wenn die Antwort 404 oder 400 mit einer Grundmeldung wäre

Liege ich falsch?

Carmine Ingaldi
quelle
1
Ich denke, Sie legen zu viel Wert auf die URI-Struktur. Der einzige Weg, um zu "Tiere /: animalID / Eier" zu gelangen, ist über HATEOAS. Sie würden das Tier also zuerst über "Tiere /: animalID" anfordern und dann für diejenigen Tiere, die Eier haben können, einen Link zu "Tiere /: animalID / Eier" geben, und für diejenigen, die dies nicht tun, wird es keinen geben sei eine Verbindung, um vom Tier zu den Eiern zu gelangen. Wenn jemand irgendwie bei Eiern für ein Tier landet, das keine Eier haben kann, geben Sie den entsprechenden HTTP-Statuscode zurück (zum Beispiel nicht gefunden oder verboten)
wired_in
0

Ja, du liegst falsch. Auch Beziehungen können gemäß den OpenAPI-Spezifikationen modelliert werden, z. B. auf diese polymorphe Weise.

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

...

Andreas Gaus
quelle
Zusätzlicher Kommentar: Das Fokussieren der API-Route GET chicken/eggs sollte auch mit den gängigen OpenAPI-Codegeneratoren für die Controller funktionieren, aber ich habe dies noch nicht überprüft. Vielleicht kann es jemand versuchen?
Andreas Gaus