web-dev-qa-db-fra.com

Conception d'une REST api par URI vs chaîne de requête

Disons que j'ai trois ressources qui sont liées comme ceci:

Grandparent (collection) -> Parent (collection) -> and Child (collection)

Ce qui précède illustre la relation entre ces ressources de la manière suivante: chaque grand-parent peut correspondre à un ou plusieurs parents. Chaque parent peut correspondre à un ou plusieurs enfants. Je veux pouvoir prendre en charge la recherche par rapport à la ressource enfant mais avec les critères de filtre:

Si mes clients me transmettent une référence d'identification à un grand-parent, je souhaite rechercher uniquement les enfants qui sont les descendants directs de ce grand-parent.

Si mes clients me transmettent une référence d'identification à un parent, je souhaite uniquement rechercher les enfants qui sont les descendants directs de mon parent.

J'ai pensé à quelque chose comme ça:

GET /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text}

et

GET /myservice/api/v1/parents/{parentID}/children?search={text}

pour les exigences ci-dessus, respectivement.

Mais je pourrais aussi faire quelque chose comme ça:

GET /myservice/api/v1/children?search={text}&grandparentID={id}&parentID=${id}

Dans cette conception, je pouvais permettre à mon client de me transmettre l'un ou l'autre dans la chaîne de requête: grandparentID ou parentID, mais pas les deux.

Mes questions sont:

1) Quelle conception d'API est la plus reposante et pourquoi? Sémantiquement, ils signifient et se comportent de la même manière. La dernière ressource dans l'URI est "enfants", ce qui implique effectivement que le client fonctionne sur la ressource enfants.

2) Quels sont les avantages et les inconvénients de chacun en termes de compréhensibilité du point de vue du client et de maintenabilité du point de vue du concepteur.

3) À quoi servent les chaînes de requête, outre le "filtrage" sur votre ressource? Si vous optez pour la première approche, le paramètre de filtre est incorporé dans l'URI lui-même en tant que paramètre de chemin d'accès au lieu d'un paramètre de chaîne de requête.

Merci!

76
HiChews123

Première

Selon RFC 3986 §3.4 (Identificateurs de ressources uniformes § (Composants de syntaxe) | Requête

3.4 Requête

Le composant de requête contient des données non hiérarchiques qui, avec les données du composant chemin (section 3.3), servent à identifier une ressource dans le cadre du schéma de l'URI et de l'autorité de dénomination (le cas échéant).

Les composants de requête sont destinés à la récupération de données non hiérarchiques; il y a peu de choses de nature plus hiérarchique qu'un arbre généalogique! Ergo que vous pensiez que c'est "REST-y" ou non - afin de vous conformer aux formats, protocoles et frameworks de et pour développer des systèmes sur Internet, vous ne devez pas utilisez la chaîne de requête pour identifier ces informations.

REST n'a rien à voir avec cette définition.

Avant de répondre à vos questions spécifiques, votre paramètre de requête "recherche" est mal nommé. Il serait préférable de traiter votre segment de requête comme un dictionnaire de paires clé-valeur.

Votre chaîne de requête pourrait être mieux définie comme

?first_name={firstName}&last_name={lastName}&birth_date={birthDate} etc.

Pour répondre à vos questions spécifiques

1) Quelle conception d'API est la plus reposante et pourquoi? Sémantiquement, ils signifient et se comportent de la même manière. La dernière ressource dans l'URI est "enfants", ce qui implique effectivement que le client fonctionne sur la ressource enfants.

Je ne pense pas que ce soit aussi clair que vous semblez le croire.

Aucune de ces interfaces de ressources n'est RESTful. La principale condition pour le style architectural RESTful est que les transitions d'état d'application doivent être communiquées depuis le serveur en tant qu'hypermédia. Les gens ont travaillé sur la structure des URI pour en faire en quelque sorte des "URI RESTful" mais la littérature formelle concernant REST a en fait très peu à dire à ce sujet. Mon opinion personnelle est qu'une grande partie des méta- désinformation à propos de REST a été publié dans le but de briser les vieilles et mauvaises habitudes. (Construire un système vraiment "RESTful" est en fait un peu de travail. L'industrie est passée à "REST" et vice versa) -a répondu à certaines préoccupations orthogonales avec des restrictions et des restrictions absurdes.)

Ce que la littérature REST dit, c'est que si vous allez utiliser HTTP comme protocole d'application, vous devez respecter les exigences formelles des spécifications du protocole et vous ne pouvez pas "créer http au fur et à mesure que vous allez" et déclarez toujours que vous utilisez http ". Si vous comptez utiliser des URI pour identifier vos ressources, vous devez respecter les exigences formelles des spécifications concernant les URI/URL.

Votre question est traitée directement par le RFC3986 §3.4, que j'ai lié ci-dessus. L'essentiel à ce sujet est que même si un URI conforme est insuffisant pour considérer une API "RESTful", si vous voulez que votre système soit "RESTful" et que vous utilisez HTTP et URIs , vous ne pouvez pas identifier les données hiérarchiques via la chaîne de requête car:

3.4 Requête

Le composant de requête contient des données non hiérarchiques

...c'est aussi simple que ça.

2) Quels sont les avantages et les inconvénients de chacun en termes de compréhensibilité du point de vue du client et de maintenabilité du point de vue du concepteur.

Les "pros" des deux premiers sont qu'ils sont sur le bon chemin. Le "contre" du troisième est qu'il semble être complètement faux.

En ce qui concerne vos problèmes de compréhensibilité et de maintenabilité, ceux-ci sont certainement subjectifs et dépendent du niveau de compréhension du développeur client et des cotes de conception du concepteur. La spécification d'URI est la réponse définitive quant à la façon dont les URI sont censés être formatés. Les données hiérarchiques sont censées être représentées sur le chemin et avec des paramètres de chemin. Les données non hiérarchiques sont censées être représentées dans la requête. Le fragment est plus compliqué, car sa sémantique dépend spécifiquement du type de média de la représentation demandée. Donc, pour répondre au volet "compréhensibilité" de votre question, je vais essayer de traduire exactement ce que vos deux premiers URI disent réellement. Ensuite, j'essaierai de représenter ce que vous dites que vous essayez d'accomplir avec des URI valides.

Traduction de vos URI textuels dans leur signification sémantique /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text} Cela dit pour les parents de grands-parents, trouvez que leur enfant a search={text} Ce que vous avez dit avec votre URI n'est cohérent que si vous recherchez les frères et sœurs d'un grand-parent. Avec vos "grands-parents, parents, enfants", vous avez trouvé un "grand-parent" remonté d'une génération à leurs parents puis redescendu à la génération des "grands-parents" en regardant les enfants des parents.

/myservice/api/v1/parents/{parentID}/children?search={text} Cela signifie que pour le parent identifié par {parentID}, trouvez que son enfant a ?search={text} Ceci est plus proche de corriger ce que vous voulez, et représente une relation parent-> enfant qui peut probablement être utilisée pour modéliser votre API entière. Pour le modéliser de cette façon, il incombe au client de reconnaître que s'il a un "grandparentId", qu'il existe une couche d'indirection entre l'ID dont il dispose et la partie du graphique familial qu'il souhaite voir. Pour trouver un "enfant" par "grandparentId", vous pouvez appeler votre /parents/{parentID}/children service, puis pour chaque enfant retourné, recherchez l'identifiant de votre personne dans ses enfants.

Implémentation de vos exigences en tant qu'URI Si vous voulez modéliser un identifiant de ressource plus extensible qui peut parcourir l'arborescence, je peux penser à plusieurs façons d'y parvenir .

1) Le premier, auquel j'ai déjà fait allusion. Représentez le graphique de "People" comme une structure composite. Chaque personne a une référence à la génération au-dessus d'elle à travers son chemin de parents et à une génération au-dessous d'elle à travers son chemin d'enfants.

/Persons/Joe/Parents/Mother/Parents serait un moyen de saisir les grands-parents maternels de Joe.

/Persons/Joe/Parents/Parents serait un moyen de saisir tous les grands-parents de Joe.

/Persons/Joe/Parents/Parents?id={Joe.GrandparentID} saisirait les grands-parents de Joe ayant l'identifiant que vous avez en main.

et ceux-ci auraient tous un sens (notez qu'il pourrait y avoir une pénalité de performance ici en fonction de la tâche en forçant un dfs sur le serveur en raison d'un manque d'identification de branche dans le modèle "Parents/Parents/Parents".) Vous bénéficiez également d'avoir la capacité de supporter n'importe quel nombre arbitraire de générations. Si, pour une raison quelconque, vous souhaitez rechercher 8 générations, vous pouvez représenter cela comme

/Persons/Joe/Parents/Parents/Parents/Parents/Parents/Parents/Parents/Parents?id={Joe.NotableAncestor}

mais cela conduit à la deuxième option dominante pour représenter ces données: via un paramètre de chemin.


2) Utilisez les paramètres de chemin pour "interroger la hiérarchie" Vous pouvez développer la structure suivante pour aider à alléger le fardeau pour les consommateurs et toujours avoir une API qui a du sens .

Pour regarder en arrière 147 générations, représenter cet identifiant de ressource avec des paramètres de chemin vous permet de faire

/Persons/Joe/Parents;generations=147?id={Joe.NotableAncestor}

Pour localiser Joe de son arrière-grand-parent, vous pouvez regarder dans le graphique un nombre connu de générations pour Joe's Id. /Persons/JoesGreatGrandparent/Children;generations=3?id={Joe.Id}

La principale chose à noter avec ces approches est que sans informations supplémentaires dans l'identifiant et la demande, vous devez vous attendre à ce que le premier URI récupère une personne 147 générations plus haut de Joe avec l'identifiant de Joe.NotableAncestor. Vous devez vous attendre à ce que le second récupère Joe. Supposons que ce que vous voulez réellement, c'est que votre client appelant puisse récupérer l'ensemble complet des nœuds et leurs relations entre la personne racine et le contexte final de votre URI. Vous pouvez le faire avec le même URI (avec une décoration supplémentaire) et en définissant un Accept de text/vnd.graphviz sur votre demande, qui est le type de support enregistré par l'IANA pour le .dot représentation graphique. Avec cela, changez l'URI en

/Persons/Joe/Parents;generations=147?id={Joe.NotableAncestor.Id}#directed

avec un en-tête de requête HTTP Accept: text/vnd.graphviz et vous pouvez faire en sorte que les clients communiquent assez clairement qu'ils veulent le graphe orienté de la hiérarchie générationnelle entre Joe et 147 générations antérieures, où cette 147e génération ancestrale contient une personne identifiée comme "l'ancêtre notable" de Joe.

Je ne sais pas si text/vnd.graphviz a une sémantique prédéfinie pour son fragment; je n'ai pu en trouver aucune dans une recherche d'instructions. Si ce type de média possède effectivement des informations de fragment prédéfinies, sa sémantique doit être suivie pour créer un URI conforme. Mais, si ces sémantiques ne sont pas prédéfinies, la spécification URI indique que la sémantique de l'identifiant de fragment n'est pas contrainte et est plutôt définie par le serveur, ce qui rend cette utilisation valide.


3) À quoi servent les chaînes de requête, outre le "filtrage" sur votre ressource? Si vous optez pour la première approche, le paramètre de filtre est incorporé dans l'URI lui-même en tant que paramètre de chemin d'accès au lieu d'un paramètre de chaîne de requête.

Je crois que j'ai déjà complètement battu cela à mort, mais les chaînes de requête ne sont pas destinées à "filtrer" les ressources. Ils sont pour identifier votre ressource à partir de données non hiérarchiques. Si vous avez exploré votre hiérarchie avec votre chemin en allant /person/{id}/children/ et vous souhaitez identifier un spécifique enfant ou un spécifique ensemble d'enfants, vous utiliseriez un attribut qui s'applique à l'ensemble que vous identifiez et l'inclure dans la requête.

66
K. Alan Bates

C'est là que vous vous trompez:

Si mes clients me transmettent une référence d'identification

Dans un système REST, le client ne devrait jamais être dérangé par des ID. Les seuls identifiants de ressource que le client devrait connaître devraient être des URI. C'est le principe de "l'interface uniforme".

Pensez à la façon dont les clients interagiraient avec votre système. Supposons que l'utilisateur commence à parcourir une liste de grands-parents, il a choisi l'un des enfants de ses grands-parents, ce qui l'amène à /grandparent/123. Si le client doit pouvoir rechercher les enfants de /grandparent/123, puis selon "HATEOAS", tout ce qui est retourné lorsque vous effectuez une requête sur /grandparent/123 devrait renvoyer une URL à l'interface de recherche. Cette URL doit déjà contenir toutes les données nécessaires pour filtrer par le grand-parent actuel intégré.

Si le lien ressemble à /grandparent/123?search={term} ou /parent?grandparent=123&search={term} ou /parent?grandparentTerm=someterm&someothergplocator=blah&search={term} sont sans conséquence selon REST. Notez que toutes ces URL ont le même nombre de paramètres, qui est {term}, même s'ils utilisent des critères différents. Vous pouvez basculer entre l'une de ces URL ou les mélanger en fonction des grands-parents spécifiques et le client ne se rompra pas, car la relation logique entre les ressources est la même, même si l'implémentation sous-jacente peut différer considérablement.

Si vous aviez créé le service de telle sorte qu'il nécessite /grandparent/{grandparentID}?search={term} quand vous allez dans un sens mais /children?parent={parentID}&search={term} a} quand vous allez dans une autre direction, c'est trop de couplage car le client devrait savoir interpoler différentes choses sur des relations différentes qui sont conceptuellement similaires.

Que vous alliez réellement avec /grandparent/123?search={term} ou /parent?grandparent=123&search={term} est une question de goût et quelle que soit la mise en œuvre la plus facile pour vous en ce moment. L'important est de ne pas exiger de modification du client si vous modifiez votre stratégie d'URL ou si vous utilisez des stratégies différentes dans différentes relations parents-enfants.

14
Lie Ryan

Je ne sais pas pourquoi les gens pensent que mettre les valeurs d'ID dans l'URL signifie que c'est en quelque sorte une REST API, REST concerne la gestion des verbes, le passage des ressources).

Donc, si vous souhaitez METTRE un nouvel utilisateur, vous devrez envoyer un bon nombre de données et une demande http POST http est idéale, donc bien que vous puissiez envoyer la clé (par exemple, l'ID utilisateur) ), vous enverrez les données utilisateur (par exemple, nom, adresse) en tant que données POST.

Maintenant, c'est un idiome commun de mettre l'identifiant de ressource dans l'URI, mais c'est plus une convention que toute forme de canonique "ce n'est pas REST si ce n'est pas dans l'URI". N'oubliez pas que - la thèse originale of REST ne mentionne pas vraiment http du tout, c'est un idiome pour gérer les données dans un client-serveur, pas quelque chose qui est une extension de http (bien que , évidemment, http est notre principale forme d'implémentation de REST).

Par exemple, Fielding utilise la demande d'un article académique comme exemple. Vous souhaitez récupérer la ressource "Document du Dr John sur la consommation de bières", mais vous pouvez également souhaiter la version initiale ou la dernière version, de sorte que l'identifiant de la ressource peut ne pas être quelque chose qui est facilement référencé comme un identifiant unique qui peut être placé dans l'URI. REST permet cela et une intention déclarée derrière cela est:

REST s'appuie plutôt sur l'auteur qui choisit un identifiant de ressource qui correspond le mieux à la nature du concept identifié

Rien ne vous empêche donc d'utiliser un URI statique pour récupérer vos parents, en passant un terme de recherche dans la chaîne de requête pour identifier l'utilisateur exact que vous recherchez. Dans ce cas, la "ressource" que vous identifiez est l'ensemble des grands-parents (et donc l'URI contient des "grands-parents" dans le cadre de l'URI. REST inclut le concept de "données de contrôle" qui est conçu pour déterminer quelle représentation de votre ressource doit être récupérée - l'exemple donné dans ces derniers est le contrôle du cache, mais aussi le contrôle de version - donc ma demande pour l'excellent document du Dr John peut être affinée en passant la version comme données de contrôle, non partie de l'URI.

Je pense qu'un exemple d'interface REST qui n'est pas habituellement mentionné est SMTP . Lors de la construction d'un message électronique, vous envoyez des verbes (FROM, TO etc) avec les données de ressource pour chaque partie du message électronique. C'est RESTful même s'il n'utilise pas les verbes http, il utilise son propre ensemble.

Donc ... bien que vous ayez besoin d'avoir une identification de ressource dans votre URI, cela ne doit pas être votre référence d'identification. Cela peut heureusement être envoyé en tant que données de contrôle, dans la chaîne de requête ou même dans les données POST. Ce que vous identifiez vraiment dans votre REST, c'est que vous êtes après un enfant, que vous avez déjà dans votre URI.

Donc, à mon avis, en lisant la définition de REST, vous demandez une ressource enfant, en transmettant des données de contrôle (sous forme de chaîne de requête) pour déterminer celle que vous souhaitez renvoyer. Par conséquent, vous ne pouvez pas faire de demande d'URI pour un grand-parent ou un parent. Vous voulez que les enfants soient renvoyés de sorte que le terme parent/grand-parent ou id ne devrait certainement pas être dans un tel URI. Les enfants devraient l'être.

10
gbjbaanb

Beaucoup de gens ont déjà parlé de ce que REST signifie, etc. etc. Mais aucun ne semble résoudre le vrai problème: votre conception.

Pourquoi les grands-parents sont-ils différents d'un père? ils ont tous les deux des enfants qui peuvent avoir des enfants qui peuvent ...

Finalement, ils sont tous "humains". Vous avez probablement cela aussi dans votre code. Alors utilisez ça:

GET /<api-endpoint>/humans/<ID>

Retournera quelques informations utiles sur l'humain. (Nom et autres)

GET /<api-endpoint>/humans/<ID>/children

retournera évidemment un tableau d'enfants. Si aucun enfant n'existe, un tableau vide est probablement le chemin à parcourir.

Pour vous faciliter la tâche, vous pouvez par exemple ajouter un indicateur "hasChildren". ou 'hasGrandChildren'.

Penser plus intelligemment, pas plus dur

4
Pinoniq

Ce qui suit est plus RESTfull car chaque grandparentID obtient sa propre URL. De cette façon, la ressource est identifiée de manière unique.

GET /myservice/api/v1/grandparents/{grandparentID}
GET /myservice/api/v1/grandparents/{grandparentID}/parents/children?search={text}

La recherche de paramètres de requête est un bon moyen d'exécuter une recherche à partir du contexte de cette ressource.

Lorsqu'une famille devient très grande, vous pouvez utiliser start/limit comme options de requête, par exemple:

GET /myservice/api/v1/grandparents/{grandparentID}/children?start=1&limit=50

En tant que développeur, il est bon d'avoir différentes ressources avec une URL/URI unique. Je pense que vous ne devriez utiliser le paramètre de requête que lorsqu'ils pourraient également être omis.

Peut-être que c'est une bonne lecture http://www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling et sinon la thèse de doctorat originale de Roy T Fielding https://www.ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation.pdf qui explique les concepts très bien et complet.

1
l.renkema

Je vais avec

/myservice/api/v1/people/{personID}/descendants/2?search={text}

Dans ce cas, chaque préfixe est une ressource valide:

  • /myservice/api/v1/people: tout le monde
  • /myservice/api/v1/people/{personID}: une personne avec des informations complètes (y compris les ancêtres, frères et sœurs, descendants)
  • /myservice/api/v1/people/{personID}/descendants: descendants d'une personne
  • /myservice/api/v1/people/{personID}/descendants/2: petits-enfants d'une personne

Ce n'est peut-être pas la meilleure réponse, mais cela a du sens pour moi.

1
laike9m