Comment modéliser une API RESTful avec héritage?

87

J'ai une hiérarchie d'objets que je dois exposer via une API RESTful et je ne sais pas comment mes URL doivent être structurées et ce qu'elles doivent renvoyer. Je n'ai trouvé aucune meilleure pratique.

Disons que j'ai des chiens et des chats héritant d'animaux. J'ai besoin d'opérations CRUD sur les chiens et les chats; Je veux aussi pouvoir faire des opérations sur les animaux en général.

Ma première idée était de faire quelque chose comme ça:

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

Le fait est que la collection / animals est désormais "incohérente", car elle peut retourner et prendre des objets qui n'ont pas exactement la même structure (chiens et chats). Est-il considéré comme "RESTful" d'avoir une collection renvoyant des objets ayant des attributs différents?

Une autre solution serait de créer une URL pour chaque type concret, comme ceci:

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

Mais maintenant, la relation entre les chiens et les chats est perdue. Si l'on souhaite récupérer tous les animaux, les ressources du chien et du chat doivent être interrogées. Le nombre d'URL augmentera également avec chaque nouveau sous-type d'animal.

Une autre suggestion était d'augmenter la deuxième solution en ajoutant ceci:

GET /animals    # get common attributes of all animals

Dans ce cas, les animaux renvoyés ne contiendraient que des attributs communs à tous les animaux, laissant tomber des attributs spécifiques au chien et au chat. Cela permet de récupérer tous les animaux, mais avec moins de détails. Chaque objet retourné peut contenir un lien vers la version détaillée et concrète.

Des commentaires ou suggestions?

Alpha Hydrae
la source

Réponses:

41

Je voudrais suggerer:

  • Utilisation d'un seul URI par ressource
  • Différencier les animaux uniquement au niveau des attributs

La configuration de plusieurs URI sur la même ressource n'est jamais une bonne idée car elle peut entraîner de la confusion et des effets secondaires inattendus. Compte tenu de cela, votre URI unique devrait être basé sur un schéma générique comme /animals.

Le prochain défi de traiter l'ensemble de la collection de chiens et de chats au niveau «de base» est déjà résolu grâce à l' /animalsapproche URI.

Le dernier défi de traiter des types spécialisés comme les chiens et les chats peut être facilement résolu en utilisant une combinaison de paramètres de requête et d'attributs d'identification dans votre type de média. Par exemple:

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 - obtient tous les chiens et chats, reviendrait à la fois Rex et mitaines
  • GET /animals?type=dog - obtient tous les chiens, ne retournerait que Rex
  • GET /animals?type=cat - obtient tous les chats, ne ferait que des mitaines

Ensuite, lors de la création ou de la modification des animaux, il appartiendrait à l'appelant de préciser le type d'animal concerné:

Type de support: application/vnd.vet-services.animal+json

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

La charge utile ci-dessus peut être envoyée avec une demande POSTou PUT.

Le schéma ci-dessus vous donne les caractéristiques de base similaires à l'héritage OO via REST, et avec la possibilité d'ajouter des spécialisations supplémentaires (c'est-à-dire plus de types d'animaux) sans chirurgie majeure ni aucune modification de votre schéma URI.

Brian Kelly
la source
Cela semble très similaire au "casting" via une API REST. Cela me rappelle également les problèmes / solutions dans la disposition de la mémoire d'une sous-classe C ++. Par exemple, où et comment représenter simultanément une base et une sous-classe avec une seule adresse en mémoire.
trcarden
10
Je suggère: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
dipold
1
En plus de spécifier le type souhaité en tant que paramètre de requête GET: il me semble que vous pouvez également utiliser accept type pour y parvenir. C'est-à-dire: GET /animals Acceptezapplication/vnd.vet-services.animal.dog+json
BrianT.
22
Et si le chat et le chien ont chacun des propriétés uniques? Comment géreriez-vous cela lors de l' POSTopération, car la plupart des frameworks ne sauraient pas comment le désérialiser correctement dans un modèle car json ne contient pas de bonnes informations de frappe. Comment géreriez-vous les cas de poste, par exemple [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}?
LB2 du
1
Et si les chiens et les chats avaient (la majorité) des propriétés différentes? par exemple # 1 POSTER une communication pour SMS (à, masque) par rapport à un e-mail (adresse e-mail, cc, cci, à, de, isHtml), ou par exemple # 2 POSTER une FundingSource pour CreditCard (maskedPan, nameOnCard, Expiration) vs. un BankAccount (bsb, accountNumber) ... utiliseriez-vous toujours une seule ressource API? Cela semblerait enfreindre la responsabilité unique des principes SOLID, mais
je
5

Cette question peut trouver une meilleure réponse avec le soutien d'une récente amélioration introduite dans la dernière version d'OpenAPI.

Il a été possible de combiner des schémas à l'aide de mots-clés tels que oneOf, allOf, anyOf et d'obtenir une charge utile de message validée depuis le schéma JSON v1.0.

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

Cependant, dans OpenAPI (anciennement Swagger), la composition des schémas a été améliorée par les mots-clés discriminator (v2.0 +) et oneOf (v3.0 +) pour véritablement prendre en charge le polymorphisme.

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

Votre héritage peut être modélisé en utilisant une combinaison de oneOf (pour choisir l'un des sous-types) et allOf (pour combiner le type et l'un de ses sous-types). Vous trouverez ci-dessous un exemple de définition de la méthode 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
dbaltor
la source
1
Il est vrai que l'OEA le permet. Cependant, il n'y a pas de support pour la fonctionnalité à afficher dans Swagger UI ( lien ), et je pense qu'une fonctionnalité est d'une utilité limitée si vous ne pouvez la montrer à personne.
emft
1
@emft, pas vrai. Au moment d'écrire cette réponse, Swagger UI prend déjà en charge cela.
Andrejs Cainikovs
Merci, cela fonctionne très bien! Il semble actuellement vrai que l'interface utilisateur de Swagger ne le montre pas complètement. Les modèles s'afficheront dans la section Schémas en bas, et toute section de réponses faisant référence à la section oneOf s'affichera partiellement dans l'interface utilisateur (schéma uniquement, pas d'exemples), mais vous n'obtiendrez aucun exemple de corps pour l'entrée de la requête. Le problème github pour cela est ouvert depuis 3 ans, il est donc probable qu'il le restera: github.com/swagger-api/swagger-ui/issues/3803
Jethro
4

J'irais pour / animals renvoyant une liste de chiens et de poissons et quoi que ce soit d'autre:

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

Il devrait être facile d'implémenter un exemple JSON similaire.

Les clients peuvent toujours compter sur la présence de l'élément «nom» (un attribut commun). Mais en fonction de l'attribut "type", il y aura d'autres éléments dans le cadre de la représentation animale.

Il n'y a rien de fondamentalement RESTful ou unRESTful à renvoyer une telle liste - REST ne prescrit aucun format spécifique pour représenter les données. Tout ce qu'il dit, c'est que les données doivent avoir une représentation et le format de cette représentation est identifié par le type de média (qui en HTTP est l'en-tête Content-Type).

Pensez à vos cas d'utilisation - avez-vous besoin d'afficher une liste d'animaux mixtes? Eh bien, retournez une liste de données animales mixtes. Avez-vous besoin d'une liste de chiens uniquement? Eh bien, faites une telle liste.

Que vous fassiez / animals? Type = dog ou / dogs n'est pas pertinent par rapport à REST qui ne prescrit aucun format d'URL - cela est laissé comme un détail d'implémentation en dehors de la portée de REST. REST indique uniquement que les ressources doivent avoir des identifiants - peu importe le format.

Vous devez ajouter des liens hypertextes pour vous rapprocher d'une API RESTful. Par exemple en ajoutant des références aux détails de l'animal:

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

En ajoutant des liens hypertextes, vous réduisez le couplage client / serveur - dans le cas ci-dessus, vous retirez le fardeau de la construction d'URL du client et laissez le serveur décider comment construire des URL (dont il est par définition la seule autorité).

Jørn Wildt
la source
1

Mais maintenant, la relation entre les chiens et les chats est perdue.

En effet, mais gardez à l'esprit que l'URI ne reflète tout simplement jamais les relations entre les objets.

G. Demecki
la source
1

Je sais que c'est une vieille question, mais je suis intéressé à étudier d'autres problèmes sur une modélisation d'héritage RESTful

Je peux toujours dire qu'un chien est un animal et une poule aussi, mais la poule fait des œufs tandis que le chien est un mammifère, donc ça ne peut pas. Une API comme

GET animaux /: animalID / oeufs

n'est pas cohérent car indique que tous les sous-types d'animaux peuvent avoir des œufs (par suite de la substitution de Liskov). Il y aurait une solution de secours si tous les mammifères répondent par «0» à cette demande, mais que se passe-t-il si j'active également une méthode POST? Dois-je avoir peur que demain il y ait des œufs de chien dans mes crêpes?

La seule façon de gérer ces scénarios est de fournir une `` super-ressource '' qui regroupe toutes les sous-ressources partagées entre toutes les `` ressources dérivées '' possibles, puis une spécialisation pour chaque ressource dérivée qui en a besoin, comme lorsque nous descendons un objet. en oop

GET / animals /: animalID / sons GET / hens /: animalID / eggs POST / hens /: animalID / eggs

L'inconvénient, ici, est que quelqu'un pourrait passer un identifiant de chien pour référencer une instance de collecte de poules, mais le chien n'est pas une poule, donc ce ne serait pas incorrect si la réponse était 404 ou 400 avec un message de raison

Ai-je tort?

Carmine Ingaldi
la source
1
Je pense que vous accordez trop d'importance à la structure URI. Le seul moyen d'accéder à "animaux /: animalID / oeufs" est de passer par HATEOAS. Donc, vous demanderiez d'abord l'animal via "animals /: animalID" et ensuite pour les animaux qui peuvent avoir des œufs, il y aura un lien vers "animals /: animalID / eggs", et pour ceux qui n'en ont pas, il n'y en aura pas être un lien pour passer de l'animal aux œufs. Si quelqu'un se retrouve d'une manière ou d'une autre avec des œufs pour un animal qui ne peut pas en avoir, renvoyez le code d'état HTTP approprié (introuvable ou interdit par exemple)
wired_in
0

Oui, vous vous trompez. Les relations peuvent également être modélisées suivant les spécifications OpenAPI, par exemple de cette manière polymorphe.

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
la source
Commentaire supplémentaire: la focalisation de la route API GET chicken/eggs devrait également fonctionner en utilisant les générateurs de code OpenAPI courants pour les contrôleurs, mais je n'ai pas encore vérifié cela. Peut-être que quelqu'un peut essayer?
Andreas Gaus le