Conception de L'API REST: collecte imbriquée vs. Nouvelle racine
cette question concerne la conception optimale de L'API REST et un problème auquel je suis confronté pour choisir entre des ressources imbriquées et des collections de niveau racine.
pour démontrer le concept, supposons que j'ai des collections City
, Business
, et Employees
. Une API typique peut être construite comme suit. Imaginez que ABC, X7N et WWW soient des clés, par exemple des guids:
GET Api/City/ABC/Businesses (returns all Businesses in City ABC)
GET Api/City/ABC/Businesses/X7N (returns business X7N)
GET Api/City/ABC/Businesses/X7N/Employees (returns all employees at business X7N)
PUT Api/City/ABC/Businesses/X7N/Employees/WWW (updates employee WWW)
cela semble propre parce qu'il suit la structure du domaine d'origine - les affaires sont dans une ville, et employés dans une entreprise. Les articles individuels sont accessibles au moyen d'une clé sous la collection (p. ex. ../Businesses
retourne toutes les entreprises, tandis que ../Businesses/X7N
retourne l'entreprise individuelle).
voici ce que le consommateur D'IPA doit pouvoir faire:
- d'entreprises dans une ville
(GET Api/City/ABC/Businesses)
- Obtenir tous les employés d'une entreprise
(GET Api/City/ABC/Businesses/X7N/Employees)
- mise à jour renseignements individuels sur l'employé
(PUT Api/City/ABC/Businesses/X7N/Employees/WWW)
ce deuxième et troisième appel, tout en semblant être au bon endroit, utilisent beaucoup de paramètres qui sont en fait inutiles.
- Pour amener les employés à une entreprise, le seul paramètre nécessaire est la clé de l'entreprise (
X7N
). - pour mettre à jour un employé, le seul paramètre nécessaire à la clé de l'employé (
WWW
)
rien dans le code d'arrière-plan n'exige de renseignements non-clés pour consulter l'entreprise ou mettre à jour l'employé. Ainsi, au lieu de cela, les paramètres suivants apparaissent mieux:
GET Api/City/ABC/Businesses (returns all Businesses in City ABC)
GET Api/Businesses/X7N (returns business X7N)
GET Api/Businesses/X7N/Employees (returns all employees at business X7N)
PUT Api/Employees/WWW (updates employee WWW)
comme vous pouvez le voir, j'ai créé une nouvelle root pour les entreprises et les employés, même si d'un point de vue de domaine, ils sont une sous-/sous-collection.
aucune des deux solutions ne me semble très propre.
- le premier exemple demande des informations inutiles, mais est structuré d'une manière qui semble "naturelle" pour le consommateur (les articles individuels d'une collection sont récupérés via des feuilles inférieures)
- le second exemple ne demande que les informations nécessaires, mais n'est pas structuré de manière "naturelle" - les sous-collections sont accessibles via les racines
- la racine de chaque employé ne fonctionnerait pas lors de l'ajout d'un nouvel employé, car nous devons savoir à quelle entreprise ajouter l'employé, ce qui signifie que cet appel devrait au moins résider sous la racine des affaires, comme
POST Api/Businesses/X7N7/Employees
, ce qui rend tout encore plus confus.
y a-t-il une troisième voie plus propre à laquelle je ne pense pas?
5 réponses
Je ne vois pas comment REST ajoute une contrainte que deux ressources ne pourraient pas avoir la même valeur. Le resourceType/ID
est juste un exemple du cas d'utilisation le plus facile plutôt que la meilleure façon d'aller D'un point de vue reposant.
Si vous lisez paragraphe 5.2.1.1 de Roy Fielding de la thèse attentivement, vous remarquerez que Fielding fait la disctinction entre un valeur et un ressource . Maintenant une ressource doit avoir une URI unique, c'est vrai. Mais rien n'empêche deux ressources d'avoir la même valeur:
par exemple, la" version préférée des auteurs "d'un article universitaire est une cartographie dont la valeur change avec le temps, alors qu'une cartographie du" document publié dans les actes de la Conférence X " est statique. Il s'agit de deux ressources distinctes, , même si elles ont toutes deux la même valeur à un moment donné. le la distinction est nécessaire pour que les deux ressources puissent être identifiées et référencées indépendamment. Un exemple similaire dans le domaine de l'ingénierie logicielle est l'identification séparée d'un fichier de code source contrôlé par version lorsqu'il est fait référence à la "dernière révision", au "numéro de révision 1.2.7" ou à la "révision incluse avec la version Orange"."
donc rien ne vous empêche de, comme vous dites, changer la racine. Dans votre exemple, une Business
est une valeur et non une ressource. Il est parfaitement reposant de créer une ressource qui est une liste de "toutes les entreprises situées dans une ville" (tout comme L'exemple de Roy, "révisions incluses avec la version Orange"), tout en ayant une ressource "business which ID is x" ainsi (comme "numéro de révision x").
pour Employees
, je garderais API/Businesses/X7N/Employees
car la relation entre une entreprise et ses employés est une composition relation, et donc comme vous dites, Employees
peut et devrait seulement être accès par la racine de la classe Businesses
. Mais ce n'est pas une exigence de REPOS, et l'autre alternative est parfaitement Reposante.
notez que cela va de pair avec l'application du principe HATEAOS. Dans votre API, la liste des entreprises situées dans une ville pourrait (et devrait peut-être d'un point de vue théorique) être juste une liste de liens vers le API/Businesses
. Mais cela signifierait que les clients auraient à faire un aller-retour vers le serveur pour chacun des éléments dans la liste. Ce n'est pas efficace, et pour rester pragmatique, ce que je fais, c'est intégrer la représentation de l'entreprise dans la liste avec le self
lien vers l'URI qui serait dans cet exemple API/Businesses
.
vous ne devez pas confondre le repos avec l'application d'une convention de nommage URI spécifique.
la façon dont les ressources sont nommées est entièrement secondaire. Vous essayez d'utiliser les conventions de nommage des ressources HTTP - cela n'a rien à voir avec REST. Roy Fielding le dit lui-même à plusieurs reprises dans les documents cités ci-dessus par d'autres. Le REPOS n'est pas un protocole, c'est un style architectural.
en fait, Roy Fielding déclare dans son commentaire 2008 blog ( http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven 6/20/2012):
"UNE API REST ne doit pas définir de ressources fixes noms ou des hiérarchies (une évidente couplage de le client et le serveur). Les serveurs doivent avoir la liberté de contrôler leur propre espace de noms. Plutôt, permettre aux serveurs de donner des instructions aux clients sur la façon de construire des URI appropriés, comme Formulaires HTML et modèles D'URI, en définissant ces instructions dans les types de médias et les liens relation."
Donc, en substance:
le problème que vous décrivez n'est pas en fait un problème de repos - conceptuellement, c'est un problème de structures hiérarchiques par rapport aux STRUCTURES relationnelles.
alors qu'une entreprise est "dans" une ville et peut donc être considérée comme faisant partie de la "hiérarchie" de la ville - qu'en est-il des entreprises internationales qui ont des bureaux dans 75 villes. Puis la ville devient soudainement l'élément junior dans une hiérarchie avec l'entreprise nom au niveau supérieur de la structure.
le fait est, vous pouvez voir des données sous différents angles, et selon le point de vue que vous prenez, il peut être plus simple de le voir comme une hiérarchie. Mais les mêmes données peuvent être considérées comme une hiérarchie à différents niveaux. Lorsque vous utilisez des noms de ressources de type HTTP, vous entrez une structure hiérarchique définie par HTTP. C'est une contrainte, Oui, mais ce n'est pas une contrainte REST, c'est une contrainte HTTP.
de l'angle, vous pouvez choisir la solution qui correspond le mieux à votre scénario. Si votre client ne peut pas fournir le nom de la ville lorsqu'il fournit le nom de l'entreprise (il ne le sait peut-être pas), alors il serait préférable d'avoir la clé avec seulement le nom de la ville. Comme je l'ai dit, c'est à vous de décider, et le repos ne vous empêchera pas ...
plus au point:
les seules contraintes de repos que vous avez, Si vous avez déjà décidé D'utiliser HTTP avec GET Mis et ainsi de suite, sont:
-
- tu ne présumeras aucune connaissance antérieure ("hors bande") entre le client et les serveurs. *
regardez votre proposition n ° 1 ci-dessus dans cette lumière. Vous supposez que les clients connaissent les clés pour les villes qui sont contenues dans votre système? Mal - ce n'est pas de tout repos. Donc, le serveur doit donner la liste des villes comme une liste de choix d'une certaine façon. Alors tu vas énumérer toutes les villes dans le monde ici? Je suppose que non, mais alors vous devrez faire un peu de travail sur la façon dont vous prévoyez de le faire, ce qui nous amène à:
-
- une API REST devrait consacrer la quasi-totalité de son travail descriptif à la définition du(des) TYPE (s) de média utilisé (s) pour représenter les ressources et piloter l'état de l'application ...
je pense que la lecture du blog mentionné Roy Fielding vous aidera considérablement.
dans une conception RESTful-API URL devrait être tout à fait sans importance - ou au moins un problème secondaire puisque la possibilité de découverte est codée dans l'hypertexte et non dans le chemin D'URL. regardez les ressources liées dans le wiki REST tag ici sur StackOverflow.
mais si vous voulez concevoir des URLs lisibles par un humain pour votre UC, je suggérerais ce qui suit:
-
Utilisez le type de ressource que vous créez/mettez à jour/interrogez comme première partie de L'URL (après le préfixe de votre API). Ainsi, lorsque quelqu'un voit L'URL, il sait immédiatement à quelles ressources cette URL renvoie.
GET /Api/Employees...
est le seul moyen de recevoir les ressources des employés de L'API. -
utilisez des identificateurs uniques pour chaque ressource indépendamment des relations qu'ils sont dans . Ainsi
GET /Api/<CollectionType>/UniqueKey
devrait retourner une ressource valide représentation. Personne ne devrait avoir à se soucier de l'endroit où se trouve l'employé. (Mais l'employé de retour doit avoir les liens avec l'entreprise (et pour des raisons de commodité ville) il appartient.)GET /Api/Employees/Z6W
renvoie L'employé avec cette ID, peu importe où est situé. -
si vous voulez obtenir une ressource spécifique: mettez votre paramètre de requête à la fin (à la place dans l'ordre hiérarchique décrit dans la question) . Vous pouvez utiliser la chaîne de requête URL (
GET /Api/Employees?City=X7N
) ou une expression de paramètre de matrice (GET /Api/Employees;City=X7N;Business=A4X,A5Y
). Cela vous permettra de facilement exprimer une collection de tous les Employés dans une Ville spécifique indépendant de l'Entreprise, ils sont.
Côté nœud:
d'après mon expérience, il est rare qu'un modèle de données de domaine hiérarchique initial Survive à des exigences supplémentaires qui surgissent au cours d'un projet. Dans votre cas: Considérez une entreprise située dans deux villes. Vous pourriez trouver une solution en le modélisant comme deux entreprises distinctes, mais qu'en est-il de l'employé qui travaille la moitié de son temps à un endroit et l'autre moitié à l'autre endroit? Ou pire: on ne sait pas pour quelle entreprise il travaille, mais dans quelle ville?
la troisième façon que je vois est de faire des entreprises et des employés ressources racine et utiliser des paramètres de requête pour filtrer les collections:
GET Api/Businesses?city=ABC (returns all Businesses in City ABC)
GET Api/Businesses/X7N (returns business X7N)
GET Api/Employees?businesses=X7N (returns all employees at business X7N)
PUT Api/Employees/WWW (updates employee WWW)
Vos deux solutions de l'utilisation du concept de RESTE sous-ressources qui exige que les sous-ressource est incluse dans les ressources pour les parents:
GET Api/City/ABC/Businesses
en réponse doit également retourner les données fournies par:
GET Api/City/ABC/Businesses/X7N
GET Api/City/ABC/Businesses/X7N/Employees
similaire pour:
GET Api/Businesses/X7N
qui doit renvoyer les données fournies par:
GET Api/Businesses/X7N/Employees
Il fera la taille de la réponse énorme et le temps requis pour générer augmentera.
faire REPOSER l'API propre à chaque ressource doit avoir qu'une seule délimitée URI qui jachère en dessous de motifs:
GET /resources
GET /resources/{id}
POST /resources
PUT /resources/{id}
si vous avez besoin de faire des liens entre les ressources utiliser HATEOAS
suivre l'exemple 1. Je ne me soucierais pas des informations inutiles du point de vue du serveur. Une URL doit clairement identifier une ressource d'une manière unique du point de vue du client. Si le client ne sait pas ce que /Employee/12
signifie sans d'abord savoir qu'il est en fait /Businesses/X7N/Employees/12
, alors la première URL semble redondante.
le client devrait traiter avec des URLs plutôt que les paramètres individuels qui composent les URLs, donc il n'y a rien de mal à avoir de longues URLs. Pour le client, ce ne sont que des ficelles. Le serveur devrait indiquer au client L'URL à faire ce qu'il a besoin de faire, et non les paramètres individuels qui exigent alors que le client construise l'URL.