API REST - Création ou mise à jour groupée en une seule demande [fermé]

92

Supposons qu'il existe deux ressources Binderet Docavec une relation d'association signifiant que le Docet Binderse tiennent seuls. Docappartenir ou non Binderet Binderêtre vide.

Si je veux concevoir une API REST qui permet à un utilisateur d'envoyer une collection de Docs, EN UNE SEULE DEMANDE , comme suit:

{
  "docs": [
    {"doc_number": 1, "binder": 1}, 
    {"doc_number": 5, "binder": 8},
    {"doc_number": 6, "binder": 3}
  ]
}

Et pour chaque doc dans le docs,

  • Si le docexiste, attribuez-le àBinder
  • Si le docn'existe pas, créez-le puis attribuez-le

Je ne sais vraiment pas comment cela devrait être implémenté:

  • Quelle méthode HTTP utiliser?
  • Quel code de réponse doit être retourné?
  • Est-ce même qualifié pour REST?
  • À quoi ressemblerait l'URI? /binders/docs?
  • Gestion des demandes groupées, que se passe-t-il si quelques éléments génèrent une erreur mais que les autres passent. Quel code de réponse doit être retourné? L'opération en bloc devrait-elle être atomique?
Sam R.
la source

Réponses:

58

Je pense que vous pouvez utiliser une méthode POST ou PATCH pour gérer cela, car ils conçoivent généralement pour cela.

  • L'utilisation d'une POSTméthode est généralement utilisée pour ajouter un élément lorsqu'elle est utilisée sur une ressource de liste, mais vous pouvez également prendre en charge plusieurs actions pour cette méthode. Consultez cette réponse: Comment mettre à jour une collection de ressources REST . Vous pouvez également prendre en charge différents formats de représentation pour l'entrée (s'ils correspondent à un tableau ou à un seul élément).

    Dans ce cas, il n'est pas nécessaire de définir votre format pour décrire la mise à jour.

  • L'utilisation d'une PATCHméthode convient également car les requêtes correspondantes correspondent à une mise à jour partielle. Selon RFC5789 ( http://tools.ietf.org/html/rfc5789 ):

    Plusieurs applications étendant le protocole HTTP (Hypertext Transfer Protocol) nécessitent une fonctionnalité pour effectuer une modification partielle des ressources. La méthode HTTP PUT existante permet uniquement un remplacement complet d'un document. Cette proposition ajoute une nouvelle méthode HTTP, PATCH, pour modifier une ressource HTTP existante.

    Dans ce cas, vous devez définir votre format pour décrire la mise à jour partielle.

Je pense que dans ce cas, POSTet PATCHsont assez similaires puisque vous n'avez pas vraiment besoin de décrire l'opération à faire pour chaque élément. Je dirais que cela dépend du format de la représentation à envoyer.

Le cas de PUTest un peu moins clair. En fait, lorsque vous utilisez une méthode PUT, vous devez fournir la liste complète. En fait, la représentation fournie dans la demande remplacera celle de la ressource de liste.

Vous pouvez avoir deux options concernant les chemins des ressources.

  • Utilisation du chemin d'accès aux ressources pour la liste de documents

Dans ce cas, vous devez fournir explicitement le lien des documents avec un classeur dans la représentation que vous fournissez dans la demande.

Voici un exemple d'itinéraire pour cela /docs.

Le contenu d'une telle approche pourrait être pour la méthode POST:

[
    { "doc_number": 1, "binder": 4, (other fields in the case of creation) },
    { "doc_number": 2, "binder": 4, (other fields in the case of creation) },
    { "doc_number": 3, "binder": 5, (other fields in the case of creation) },
    (...)
]
  • Utilisation du chemin d'accès aux ressources secondaires de l'élément de classeur

En outre, vous pouvez également envisager d'exploiter les sous-itinéraires pour décrire le lien entre les documents et les classeurs. Les conseils concernant l'association entre un document et un classeur n'ont plus à être spécifiés dans le contenu de la requête.

Voici un exemple d'itinéraire pour cela /binder/{binderId}/docs. Dans ce cas, envoyer une liste de documents avec une méthode POSTou PATCHattachera des documents au classeur avec identifiant binderIdaprès avoir créé le document s'il n'existe pas.

Le contenu d'une telle approche pourrait être pour la méthode POST:

[
    { "doc_number": 1, (other fields in the case of creation) },
    { "doc_number": 2, (other fields in the case of creation) },
    { "doc_number": 3, (other fields in the case of creation) },
    (...)
]

Concernant la réponse, c'est à vous de définir le niveau de réponse et les erreurs à renvoyer. Je vois deux niveaux: le niveau de statut (niveau global) et le niveau de charge utile (niveau plus fin). A vous aussi de définir si toutes les insertions / mises à jour correspondant à votre demande doivent être atomiques ou non.

  • Atomique

Dans ce cas, vous pouvez tirer parti de l'état HTTP. Si tout se passe bien, vous obtenez un statut 200. Sinon, un autre statut comme 400si les données fournies ne sont pas correctes (par exemple, l'ID du classeur n'est pas valide) ou autre chose.

  • Non atomique

Dans ce cas, un statut 200sera retourné et c'est à la représentation de la réponse de décrire ce qui a été fait et où les erreurs se produisent finalement. ElasticSearch a un point de terminaison dans son API REST pour la mise à jour en masse. Cela pourrait vous donner quelques idées à ce niveau: http://www.elasticsearch.org/guide/en/elasticsearch/guide/current/bulk.html .

  • Asynchrone

Vous pouvez également implémenter un traitement asynchrone pour gérer les données fournies. Dans ce cas, l'état HTTP renvoyé sera 202. Le client doit extraire une ressource supplémentaire pour voir ce qui se passe.

Avant de terminer, je voudrais également noter que la spécification OData aborde le problème des relations entre les entités avec la fonctionnalité nommée liens de navigation . Peut-être pourriez-vous jeter un œil à ceci ;-)

Le lien suivant peut également vous aider: https://templth.wordpress.com/2014/12/15/designing-a-web-api/ .

J'espère que ça vous aide, Thierry

Thierry Templier
la source
J'ai suivi sur la question. J'ai opté pour des itinéraires plats sans sous-ressource imbriquée. Pour obtenir tous les documents que j'appelle GET /docset récupérer tous les documents dans un classeur particulier GET /docs?binder_id=x. Pour supprimer un sous-ensemble des ressources, dois-je appeler DELETE /docs?binder_id=xou dois-je appeler DELETE /docsavec un {"binder_id": x}dans le corps de la demande? Souhaitez-vous jamais utiliser PATCH /docs?binder_id=xpour une mise à jour par lots, ou simplement PATCH /docset passer des paires?
Andy Fusniak
34

Vous devrez probablement utiliser POST ou PATCH, car il est peu probable qu'une seule requête qui met à jour et crée plusieurs ressources soit idempotente.

Faire PATCH /docsest définitivement une option valable. Vous pourriez trouver l'utilisation des formats de correctifs standard délicate pour votre scénario particulier. Pas sûr à ce sujet.

Vous pouvez utiliser 200. Vous pouvez également utiliser 207 - Multi Status

Cela peut être fait de manière REST. La clé, à mon avis, est d'avoir une ressource conçue pour accepter un ensemble de documents à mettre à jour / créer.

Si vous utilisez la méthode PATCH, je pense que votre opération devrait être atomique. c'est-à-dire que je n'utiliserais pas le code de statut 207 pour signaler les succès et les échecs dans le corps de la réponse. Si vous utilisez l'opération POST, l'approche 207 est viable. Vous devrez concevoir votre propre corps de réponse pour communiquer quelles opérations ont réussi et lesquelles ont échoué. Je n'en connais pas de standardisé.

Darrel Miller
la source
Merci beaucoup. En This can be done in a RESTful wayvoulez - vous dire la mise à jour et de créer doit être fait séparément?
Sam R.
1
@norbertpy L'exécution d'une sorte d'opération d'écriture sur une ressource peut entraîner la mise à jour et la création d'autres ressources à partir d'une seule requête. REST n'a aucun problème avec cela. Mon choix de phrase était parce que certains frameworks implémentaient des opérations en masse en sérialisant les requêtes HTTP dans des documents en plusieurs parties, puis en envoyant les requêtes HTTP sérialisées sous forme de lot. Je pense que cette approche viole la contrainte REST d'identification des ressources.
Darrel Miller
19

PUT ment

PUT /binders/{id}/docs Créer ou mettre à jour et associer un seul document à un classeur

par exemple:

PUT /binders/1/docs HTTP/1.1
{
  "docNumber" : 1
}

PATCH ing

PATCH /docs Créez des documents s'ils n'existent pas et associez-les à des classeurs

par exemple:

PATCH /docs HTTP/1.1
[
    { "op" : "add", "path" : "/binder/1/docs", "value" : { "doc_number" : 1 } },
    { "op" : "add", "path" : "/binder/8/docs", "value" : { "doc_number" : 8 } },
    { "op" : "add", "path" : "/binder/3/docs", "value" : { "doc_number" : 6 } }
] 

J'inclurai des informations supplémentaires plus tard, mais en attendant, si vous le souhaitez, jetez un œil à RFC 5789 , RFC 6902 et William Durand's Please. Ne pas patcher comme une entrée de blog idiot .

Mauricio Morales
la source
2
Parfois, le client a besoin d'une opération en bloc et il ne veut pas se soucier de savoir si la ressource est là ou non. Comme je l'ai dit dans la question, le client veut en envoyer un tas docset les associer binders. Le client souhaite créer des classeurs s'ils n'existent pas et faire l'association s'ils existent. Dans UNE SEULE demande EN VRAC.
Sam R.
12

Dans un projet dans lequel j'ai travaillé, nous avons résolu ce problème en implémentant quelque chose que nous appelons les requêtes «Batch». Nous avons défini un chemin /batchoù nous avons accepté json au format suivant:

[  
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 1,
         binder: 1
      }
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 5,
         binder: 8
      }
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 6,
         binder: 3
      }
   },
]

La réponse a le code d'état 207 (Multi-Status) et ressemble à ceci:

[  
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 1,
         binder: 1
      }
      status: 200
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         error: {
            msg: 'A document with doc_number 5 already exists'
            ...
         }
      },
      status: 409
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 6,
         binder: 3
      },
      status: 200
   },
]

Vous pouvez également ajouter la prise en charge des en-têtes dans cette structure. Nous avons implémenté quelque chose qui s'est avéré utile, à savoir des variables à utiliser entre les requêtes d'un lot, ce qui signifie que nous pouvons utiliser la réponse d'une requête comme entrée à une autre.

Facebook et Google ont des implémentations similaires:
https://developers.google.com/gmail/api/guides/batch
https://developers.facebook.com/docs/graph-api/making-multiple-requests

Lorsque vous souhaitez créer ou mettre à jour une ressource avec le même appel, j'utiliserais POST ou PUT selon le cas. Si le document existe déjà, voulez-vous que le document entier soit:

  1. Remplacé par le document que vous envoyez (par exemple, les propriétés manquantes dans la demande seront supprimées et déjà existantes écrasées)?
  2. Fusionné avec le document que vous envoyez (c'est-à-dire que les propriétés manquantes dans la demande ne seront pas supprimées et les propriétés déjà existantes seront écrasées)?

Dans le cas où vous voulez le comportement de l'alternative 1, vous devez utiliser un POST et si vous voulez le comportement de l'alternative 2, vous devez utiliser PUT.

http://restcookbook.com/HTTP%20Methods/put-vs-post/

Comme les gens l'ont déjà suggéré, vous pouvez également opter pour PATCH, mais je préfère garder l'API simple et ne pas utiliser de verbes supplémentaires s'ils ne sont pas nécessaires.

David Berg
la source
5
Aimez cette réponse pour le Proof-of-Concept ainsi que les liens Google et Facebook. Mais en désaccord avec la partie finale sur POST ou PUT. Dans les 2 cas mentionnés par cette réponse, la première devrait être PUT et la seconde devrait être PATCH.
RayLuo
@RayLuo, pouvez-vous expliquer pourquoi nous avons besoin de PATCH en plus de POST et PUT?
David Berg
2
Parce que c'est pour cela que le PATCH a été inventé. Vous pouvez lire cette définition et voir comment le PUT et le PATCH correspondent à vos 2 puces.
RayLuo
@DavidBerg, Il semble que Google ait préféré une autre approche pour traiter les requêtes par lots, c'est-à-dire séparer l'en-tête et le corps de chaque sous-requête dans la partie correspondante d'une requête principale, avec une limite comme --batch_xxxx. Existe-t-il des différences cruciales entre les solutions de Google et Facebook? De plus, à propos de "utiliser la réponse d'une requête comme entrée à une autre", cela semble très intéressant, pourriez-vous partager plus de détails? ou quel type de scénario doit être utilisé?
Yang