RESTFul: actions de changement d'état


60

Je prévois de construire une API RESTfull, mais certaines questions d’architecture me posent certains problèmes. L'ajout d'une logique de gestion de portefeuille aux clients est une option que j'aimerais éviter, car la mise à jour de plusieurs plates-formes clientes est difficile à gérer en temps réel, lorsque la logique de gestion peut rapidement changer.

Disons que nous avons l'article en tant que ressource (api / article), comment devrions-nous mettre en œuvre des actions telles que publier, annuler la publication, activer ou désactiver, etc., tout en essayant de le garder aussi simple que possible?

1) Devons-nous utiliser api / article / {id} / {action}, car beaucoup de logique de base peut y être appliquée, comme pousser vers des emplacements distants ou modifier plusieurs propriétés. La chose la plus difficile ici est probablement que nous devons renvoyer toutes les données d'article à l'API pour la mise à jour et que le travail multi-utilisateur ne peut pas être mis en œuvre. Par exemple, l'éditeur pourrait envoyer des données plus anciennes de 5 secondes et corriger un écrasement qu'un autre journaliste vient de faire il y a 2 secondes. Je ne peux absolument pas expliquer cela aux clients, car ceux qui publient un article ne sont aucunement liés à la mise à jour du contenu.

2) Créer une nouvelle ressource peut également être une option, api / article- {action} / id, mais la ressource renvoyée ne serait alors pas article- {action} mais un article dont je ne suis pas sûr si cela est approprié. De plus, dans la classe d’article code côté serveur, le traitement est effectué sur les deux ressources et je ne sais pas si cela va à l’encontre de la pensée RESTfull.

Toutes les suggestions sont les bienvenues ..


Il est parfaitement légal de faire en sorte que des «actions» fassent partie d'un URI RESTful, si elles indiquent une action / un algorithme à exécuter. Quel est le problème avec api/article?action=publish? Les paramètres de requête sont destinés aux cas où l'état de la ressource dépend de l'algorithme (ou de l'action) que vous mentionnez. Par exemple, api/articles?sort=ascest valide
PhD

1
Je vous suggère de consulter cet article , qui pourrait vous inspirer une solution encore plus reposante.
Benjol

Un des problèmes que je vois avec api / article? Action = publish est que dans l'application RESTfull doit envoyer TOUTES les données des articles pour publication alors que je préférerais le faire: api / article / 4545 / publish / sans autre élément
Miro Svrtan

2
Excellente ressource sur ce sujet et plus encore: Conception d'API REST - Modélisation de ressources
Izhaki

@PhD: bien que cela soit parfaitement légal dans le protocole, les URI devraient généralement être des noms plutôt que des verbes. Avoir un verbe dans l'URI est généralement le signe d'une mauvaise conception REST.
Lie Ryan

Réponses:


49

Je trouve les pratiques décrites ici utiles:

Qu'en est-il des actions qui ne s'inscrivent pas dans le monde des opérations CRUD?

C'est là que les choses peuvent devenir floues. Il y a plusieurs approches:

  1. Restructurez l'action pour qu'elle apparaisse comme un champ d'une ressource. Cela fonctionne si l'action ne prend pas de paramètres. Par exemple, une action d' activation peut être mappée sur un activatedchamp booléen et mise à jour via un PATCH vers la ressource.
  2. Traitez-le comme une sous-ressource avec les principes RESTful. Par exemple, l'API de GitHub vous permet de StAR un point essentiel avec PUT /gists/:id/staret Désactiver le suivi avec DELETE /gists/:id/star.
  3. Parfois, vous ne pouvez vraiment pas mapper l'action sur une structure RESTful sensible. Par exemple, une recherche multi-ressources n'a pas vraiment de sens pour être appliquée au noeud final d'une ressource spécifique. Dans ce cas, /searchserait le plus logique même si ce n'est pas une ressource. C'est correct - faites ce qui est juste du point de vue du consommateur d'API et assurez-vous qu'il soit documenté clairement pour éviter toute confusion.

Je vote en faveur de l'approche 2. Même si cela peut sembler être une ressource artificielle maladroite pour les appelants d'API, en réalité, ils ne savent pas ce qui se passe sur le serveur. Si j'appelle POST /article/123/deactivationspour créer une nouvelle demande de désactivation pour l'article 123, le serveur risque non seulement de désactiver la ressource demandée, mais également de stocker ma demande de désactivation afin de pouvoir récupérer son statut ultérieurement.
JustAMartin

2
pourquoi PUT /gists/:id/star pas POST /gists/:id/star?
Filip Bartuzi

11
@ FilipBartuzi Parce que PUT est idempotent - c’est-à-dire que peu importe le nombre de fois que vous exécutez une action avec les mêmes paramètres, le résultat est toujours le même (par exemple, si vous allumez une lumière, elle change. Si vous essayez de encore, rien ne change - c’est déjà). Le POST n’est pas idempotent - c’est-à-dire que chaque fois que vous exécutez une action, même avec les mêmes paramètres, l’action a un résultat différent (par exemple, si vous envoyez une lettre à une personne, cette personne reçoit une lettre. Si vous envoyez une lettre identique à la même personne, ils ont maintenant 2 lettres).
Raphaël

9

Les opérations qui entraînent des modifications majeures de l'état et du comportement côté serveur, telles que l'action "publier" que vous décrivez, sont difficiles à modéliser explicitement dans REST. Une solution que je vois souvent est de conduire implicitement un tel comportement complexe à travers les données.

Pensez à commander des produits via une API REST exposée par un commerçant en ligne. La commande est une opération complexe. Plusieurs produits seront emballés et expédiés, votre compte sera débité et vous recevrez un reçu. Vous pouvez annuler votre commande pour une durée limitée et il existe bien sûr une garantie de remboursement intégral vous permettant de renvoyer des produits pour un remboursement.

Au lieu d'une opération d'achat complexe, une telle API peut vous permettre de créer une nouvelle ressource, une commande d'achat. Au début, vous pouvez y apporter les modifications souhaitées: ajouter ou supprimer des produits, modifier l'adresse de livraison, choisir une autre option de paiement ou annuler votre commande. Vous pouvez faire tout cela parce que vous n’avez encore rien acheté, vous manipulez simplement des données sur le serveur.

Une fois votre commande terminée et le délai de grâce écoulé, le serveur verrouille votre commande pour empêcher toute modification ultérieure. Ce n’est qu’à ce moment-là que la séquence complexe d’opérations commence, mais vous ne pouvez pas la contrôler directement, mais indirectement par le biais des données que vous avez précédemment placées dans la commande.

Selon votre description, "publier" pourrait être mis en œuvre de cette manière. Au lieu d'exposer une opération, vous placez une copie du brouillon que vous avez examiné et que vous souhaitez publier en tant que nouvelle ressource sous / publier. Cela garantit que les éventuelles mises à jour ultérieures du brouillon ne seront pas publiées, même si l'opération de publication elle-même se termine des heures plus tard.


L'idée de remplacer l'intégralité de la ressource d'un article non publié par un brouillon conviendrait parfaitement à ce cas, mais ne conviendrait pas à toutes les autres actions existantes sur une ressource en général. Est-ce que REST est même supposé s'en occuper? Peut-être que j'abuse de cela et que je ne devrais l'utiliser que comme CRUD et rien de plus, comme les requêtes SQL où je ne m'attends pas à ce qu'une logique soit à l'intérieur de la requête elle-même.
Miro Svrtan

Pourquoi je demande tout ça? Depuis quelques temps déjà, les applications Web commencent à être multipliées et je préférerais garder beaucoup de logique commerciale sur le serveur, car il est pratiquement impossible de mettre à jour la logique commerciale sur iOS, Android, sur le Web, sur les ordinateurs de bureau ou sur d’autres plateformes. rapidement et je voudrais éviter tout problème de compatibilité ascendante lors du changement d’un petit morceau de BL.
Miro Svrtan

2
Je pense que REST peut parfaitement gérer la logique métier, mais il ne convient pas pour exposer la logique métier existante écrite sans avoir à l'esprit REST. C'est pourquoi de nombreuses entreprises telles que Microsoft, SAP et autres n'exposent souvent que des données à l'aide d'opérations CRUD, comme vous l'avez dit. Jetez un coup d'œil au protocole Open Data pour voir comment ils le font.
Ferenc Mihaly

7

nous devons renvoyer toutes les données d'article à l'API pour la mise à jour et le travail multi-utilisateur n'a pas pu être mis en œuvre. Par exemple, l'éditeur pourrait envoyer des données plus anciennes de 5 secondes et corriger un écrasement qu'un autre journaliste vient de faire il y a 2 secondes. Je ne peux absolument pas expliquer cela aux clients, car ceux qui publient un article ne sont aucunement liés à la mise à jour du contenu.

Peu importe ce que vous faites, ce type de problème est un défi. Il s'agit d'un problème très similaire au contrôle de source distribué (mercurial, git, etc.), et la solution, épelée dans HTTP / ReST, semble un peu similaire.

Supposons que deux utilisateurs, Alice et Bob, travaillent tous les deux /articles/lunch. (pour plus de clarté, la réponse est en gras)

Tout d’abord, alice crée l’article.

PUT /articles/lunch HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

301 Moved Permanently
Location: /articles/lunch/1 

Le serveur n'a pas créé de ressource, car il n'y avait pas de "version" attachée à la demande (en supposant un identifiant de /articles/{id}/{version}. Pour effectuer la création, Alice a été redirigée vers l'URL de l'article / de la version qu'elle va créer. L'utilisateur d'Alice L'agent réappliquera alors la demande à la nouvelle adresse.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

201 Created

Et maintenant, l'article a été créé. Ensuite, Bob regarde l'article:

GET /articles/lunch HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

301 Moved Permanently
Location: /articles/lunch/1 

Bob regarde là:

GET /articles/lunch/1 HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

200 Ok
Content-Type: text/plain

Hey Bob, what do you want for lunch today?

Il décide d'ajouter sa propre monnaie.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

301 Moved Permanently
Location: /articles/lunch/2

Comme avec Alice, Bob est redirigé vers où il créera une nouvelle version.

PUT /articles/lunch/2 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

201 Created

Finalement, Alice décide d’ajouter à son propre article:

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?
I was thinking about getting Sushi.

409 Conflict
Location: /articles/lunch/3
Content-Type: text/diff

---/articles/lunch/2
+++/articles/lunch/3
@@ 1,2 1,2 @@
 Hey Bob, what do you want for lunch today?
-Does pizza sound good to you, Alice?
+I was thinking about getting Sushi.

Au lieu d'être redirigé normalement, un code d'état différent est renvoyé au client 409, ce qui indique à Alice que la version à partir de laquelle elle essayait de créer une branche a déjà été créée. De toute façon, les nouvelles ressources ont été créées (comme le montre l'en- Locationtête) et les différences entre les deux ont été incluses dans le corps de la réponse. Alice sait maintenant que la demande qu'elle vient de faire doit être fusionnée en quelque sorte.


Toute cette redirection est liée à la sémantique de PUT, ce qui nécessite la création de nouvelles ressources exactement à l'endroit demandé par la ligne de requête. cela pourrait aussi économiser un cycle de requête en utilisant la POSTplace, mais le numéro de version devrait alors être encodé dans la requête par une autre magie, ce qui me paraissait moins évident aux fins d'illustration, mais serait probablement préféré dans une vraie API. minimiser les cycles de demande / réponse.


1
Le verson n'est pas un problème ici, je viens de le dire comme exemple de problèmes possibles si vous utilisez un article comme ressource pour publier une action
Miro Svrtan

3

Voici un autre exemple qui traite non pas du contenu des documents mais davantage de l’état transitoire. (Je trouve des versions - étant donné qu'en général, chaque version peut constituer une nouvelle ressource - une sorte de problème facile.)

Supposons que je souhaite exposer un service exécuté sur une machine via un REST afin qu'il puisse être arrêté, démarré, redémarré, etc.

Quelle est l'approche la plus reposante ici? POST / service? Commande = redémarrer, par exemple? Ou POST / service / état avec un corps de, disons, «en cours d'exécution»?

Il serait bien de codifier ici les meilleures pratiques et de savoir si REST est la bonne approche dans ce type de situation.

Deuxièmement, supposons que je souhaite diriger une action d’un service qui n’affecte pas son propre état, mais déclenche plutôt un effet secondaire. Par exemple, un service de messagerie qui envoie un rapport, créé au moment de l'appel, à plusieurs adresses e-mail.

GET / report peut être un moyen d’obtenir moi-même une copie du rapport; mais que se passe-t-il si nous voulons pousser du côté serveur d'autres actions telles que l'envoi par courrier électronique, comme je le dis ci-dessus Ou écrire dans une base de données.

Ces cas vont dans le sens de la fracture ressources-actions, et je vois des moyens de les gérer de manière orientée REST, mais franchement, c'est un peu comme un bidouillage. La question clé est peut-être de savoir si une API REST devrait prendre en charge les effets secondaires en général.


2

REST est orienté données et, de ce fait, les ressources fonctionnent mieux comme des "choses" que des actions. La sémantique implicite des méthodes http; GET, PUT, DELETE, etc. servent à renforcer l’orientation. POST bien sûr, est l'exception.

Une ressource peut être un mélange de données c'est-à-dire. contenu de l'article; et métadonnées c'est-à-dire. publié, verrouillé, révision. Il existe de nombreuses autres manières possibles de découper les données, mais vous devez tout d’abord définir l’apparence du flux de données afin de déterminer celui qui est le plus optimal (s’il en existe un). Par exemple, il se peut que les révisions soient leur propre ressource dans l'article, comme le suggère TokenMacGuy.

En ce qui concerne la mise en œuvre, je ferais probablement quelque chose comme ce que suggère TockenMacGuy. J'ajouterais également des champs de métadonnées sur article, pas de révision, comme "verrouillés" et "publiés".


1

Ne pensez pas que cela manipule directement l'état de l'article. Au lieu de cela, vous créez un ordre de modification pour demander la création de l'article.

Vous pouvez modéliser la mise en ordre de modification en créant une nouvelle ressource d'ordre de modification (POST). Il y a beaucoup d'avantages. Par exemple, vous pouvez spécifier une date et une heure futures pour la publication de l'article dans l'ordre de modification, et laisser le serveur se soucier de la mise en œuvre.

Si la publication n'est pas un processus instantané, il n'est pas nécessaire d'attendre la fin de l'opération pour revenir au client. Vous venez de reconnaître que la demande de changement a été créée et de renvoyer l'ID de demande de changement. Vous pouvez ensuite utiliser l'URL correspondant à cette demande de changement pour partager le statut de celle-ci.

L'un des éléments clés pour moi a été de reconnaître que cette métaphore d'ordre de changement n'est qu'un autre moyen de décrire la programmation orientée objet. Au lieu de ressources, nous appelons alors des objets. Au lieu de commandes de changement, nous les appelons des messages. Une façon d'envoyer un message de A à B dans OO consiste à faire en sorte que A appelle une méthode sur B. Une autre façon de le faire, en particulier lorsque A et B se trouvent sur des ordinateurs différents, consiste à faire en sorte que A crée un nouvel objet, M, et envoyez-le à B. REST formalise simplement ce processus.


En fait, je considérerais cela plus proche du modèle de l'acteur que de OO. Considérer les demandes REST comme des messages (ce qu’elles sont) les aligne parfaitement sur Event Sourcing pour la gestion de l’état. REST divise soigneusement ses interactions le long des lignes CQRS. Les messages GET correspondent aux requêtes, POST, PUT, PATCH, DELETE dans la zone de commande.
WillD

0

Si je vous ai bien compris, je pense que ce que vous avez est davantage un problème de détermination de "règle de gestion" qu'un problème technique.

Le fait qu’un article puisse être écrasé peut être résolu en introduisant des niveaux d’autorisation permettant aux utilisateurs expérimentés de remplacer les versions des utilisateurs juniors.En introduisant également des versions, ainsi qu’une colonne permettant de saisir l’état de l’article (par exemple, 'en développement', 'final' , etc.), vous pourriez surmonter cela. Vous pouvez également donner à l'utilisateur la possibilité de sélectionner une version donnée en combinant le moment de la soumission et le numéro de version.

Dans tous les cas ci-dessus, votre service doit implémenter les règles métier que vous avez définies. Vous pouvez donc appeler le service avec les paramètres suivants: userid, article, version, action (où la version est facultative, cela dépend également de vos règles commerciales).


Je ne crois pas qu'il s'agisse d'une règle de commerce mais d'une règle technique. L’idée d’ajouter une version est un bon moyen d’aider à redéfinir les règles, mais ne résout toujours pas le fait que la mise à jour du contenu et la publication du contenu ne sont pas des actions liées.
Miro Svrtan
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.