web-dev-qa-db-fra.com

Modèles de traitement des opérations par lots dans REST services Web?

Quels modèles de conception éprouvés existent pour les opérations par lots sur les ressources dans un service Web de style REST?

J'essaie de trouver un équilibre entre idéaux et réalité en termes de performance et de stabilité. Nous avons actuellement une API dans laquelle toutes les opérations sont extraites d'une ressource de liste (par exemple, GET/utilisateur) ou sur une seule instance (PUT/utilisateur/1, DELETE/utilisateur/22, etc.).

Dans certains cas, vous souhaitez mettre à jour un seul champ d'un ensemble d'objets. Il semble très inutile d'envoyer la représentation entière de chaque objet dans les deux sens pour mettre à jour le champ.

Dans une API de style RPC, vous pourriez avoir une méthode:

/mail.do?method=markAsRead&messageIds=1,2,3,4... etc. 

Quelle est l'équivalent REST ici? Ou est-il correct de faire des compromis de temps en temps? Cela ruine-t-il la conception d'ajouter quelques opérations spécifiques qui améliorent réellement les performances, etc.? Le client en tout est actuellement un navigateur Web (application javascript côté client).

165
Mark Renouf

Un modèle simple RESTful pour les lots consiste à utiliser une ressource de collection. Par exemple, pour supprimer plusieurs messages à la fois.

DELETE /mail?&id=0&id=1&id=2

Il est un peu plus compliqué de mettre à jour par lots des ressources partielles ou des attributs de ressources. C'est-à-dire, mettez à jour chaque attribut marquéAsRead. Fondamentalement, au lieu de traiter l'attribut comme faisant partie de chaque ressource, vous le traitez comme un seau dans lequel placer des ressources. Un exemple a déjà été posté. Je l'ai ajusté un peu.

POST /mail?markAsRead=true
POSTDATA: ids=[0,1,2]

Fondamentalement, vous mettez à jour la liste des messages marqués comme lus.

Vous pouvez également l'utiliser pour affecter plusieurs éléments à la même catégorie.

POST /mail?category=junk
POSTDATA: ids=[0,1,2]

Il est évidemment beaucoup plus compliqué d'effectuer des mises à jour partielles par lots de style iTunes (par exemple, artiste + albumTitle mais pas trackTitle). L'analogie de seau commence à s'effondrer.

POST /mail?markAsRead=true&category=junk
POSTDATA: ids=[0,1,2]

À long terme, il est beaucoup plus facile de mettre à jour une ressource partielle unique, ou des attributs de ressource. Utilisez simplement une sous-ressource.

POST /mail/0/markAsRead
POSTDATA: true

Vous pouvez également utiliser des ressources paramétrées. Ceci est moins courant dans les modèles REST, mais est autorisé dans les spécifications URI et HTTP. Un point-virgule divise les paramètres liés horizontalement au sein d'une ressource.

Mettez à jour plusieurs attributs, plusieurs ressources:

POST /mail/0;1;2/markAsRead;category
POSTDATA: markAsRead=true,category=junk

Mettez à jour plusieurs ressources, un seul attribut:

POST /mail/0;1;2/markAsRead
POSTDATA: true

Mettez à jour plusieurs attributs, une seule ressource:

POST /mail/0/markAsRead;category
POSTDATA: markAsRead=true,category=junk

La créativité RESTful abonde.

74
Alex

Pas du tout - je pense que l'équivalent REST est (ou au moins une solution)) presque exactement cela - une interface spécialisée conçue conçue pour une opération requise par le client.

Cela me rappelle un motif mentionné dans le livre de Crane and Pascarello Ajax in Action (un excellent livre, d'ailleurs - hautement recommandé) dans lequel ils illustrent la mise en œuvre d'un CommandQueue sorte d’objet dont le travail consiste à mettre les demandes en lots, puis à les poster périodiquement sur le serveur.

Si je me souviens bien, l'objet contenait essentiellement un tableau de "commandes" - par exemple, pour étendre votre exemple, chaque enregistrement est un enregistrement contenant une commande "markAsRead", un "messageId" et peut-être une référence à un callback/handler fonction - et ensuite selon un calendrier ou une action de l'utilisateur, l'objet de commande serait sérialisé et envoyé au serveur, et le client se chargerait du post-traitement en résultant.

Les détails ne me sont pas utiles, mais il semblerait qu'une file d'attente de commandes de ce type serait un moyen de régler votre problème. cela réduirait considérablement le nombre total de discussions, et résumerait l'interface côté serveur d'une manière plus flexible.


Mise à jour : Aha! J'ai trouvé un extrait de ce livre en ligne, avec des exemples de code (bien que je suggère quand même de prendre le livre!). regardez ici , en commençant par la section 5.5.3:

Cela est facile à coder, mais peut générer beaucoup de petits flux de trafic vers le serveur, ce qui est inefficace et potentiellement déroutant. Si nous voulons contrôler notre trafic, nous pouvons capturer ces mises à jour et les mettre en file d'attente localement , puis les envoyer au serveur par lots à notre guise. Une liste de mise à jour simple implémentée en JavaScript est présentée dans la liste 5.13. [...]

La file d'attente maintient deux tableaux. queued est un tableau indexé numériquement, auquel de nouvelles mises à jour sont ajoutées. sent est un tableau associatif contenant les mises à jour envoyées au serveur mais en attente de réponse.

Voici deux fonctions pertinentes: l’une responsable de l’ajout de commandes à la file d’attente (addCommand) et l’autre de la sérialisation, puis de leur envoi au serveur (fireRequest):

CommandQueue.prototype.addCommand = function(command)
{ 
    if (this.isCommand(command))
    {
        this.queue.append(command,true);
    }
}

CommandQueue.prototype.fireRequest = function()
{
    if (this.queued.length == 0)
    { 
        return; 
    }

    var data="data=";

    for (var i = 0; i < this.queued.length; i++)
    { 
        var cmd = this.queued[i]; 
        if (this.isCommand(cmd))
        {
            data += cmd.toRequestString(); 
            this.sent[cmd.id] = cmd;

            // ... and then send the contents of data in a POST request
        }
    }
}

Cela devrait vous permettre de partir. Bonne chance!

25
Christian Nunciato

Bien que je pense que @Alex est sur la bonne voie, je pense que conceptuellement, cela devrait être l'inverse de ce qui est suggéré.

L'URL est en effet "les ressources que nous ciblons" d'où:

    [GET] mail/1

signifie obtenir l'enregistrement de mail avec l'identifiant 1 et

    [PATCH] mail/1 data: mail[markAsRead]=true

signifie patcher l'enregistrement de courrier avec l'id 1. La chaîne de requête est un "filtre" qui filtre les données renvoyées par l'URL.

    [GET] mail?markAsRead=true

Nous demandons donc ici tout le courrier déjà marqué comme lu. Donc, [PATCH] sur ce chemin dirait "corrige les enregistrements déjà marqué comme vrai" ... ce qui n'est pas ce que nous essayons de réaliser.

Donc, une méthode batch, en suivant cette pensée devrait être:

    [PATCH] mail/?id=1,2,3 <the records we are targeting> data: mail[markAsRead]=true

bien sûr, je ne dis pas que cela est vrai REST (ce qui ne permet pas la manipulation d'enregistrements par lots)), mais plutôt la logique déjà existante et utilisée par REST.

19
fezfox

Votre langage, "Il semble très inutile ...", indique une tentative d'optimisation prématurée. À moins qu'il ne soit démontré que l'envoi de la totalité de la représentation des objets est un problème majeur en termes de performances (nous disons que les utilisateurs acceptent comme supérieur à 150 ms), il est alors inutile de tenter de créer un nouveau comportement d'API non standard. N'oubliez pas que plus l'API est simple, plus il est facile à utiliser.

Pour les suppressions, envoyez ce qui suit, car le serveur n'a besoin de rien savoir de l'état de l'objet avant la suppression.

DELETE /emails
POSTDATA: [{id:1},{id:2}]

L’idée suivante est que si une application rencontre des problèmes de performances concernant la mise à jour en bloc d’objets, il convient d’envisager de diviser chaque objet en plusieurs objets. De cette façon, la charge utile JSON est une fraction de la taille.

Par exemple, lorsque vous envoyez une réponse pour mettre à jour les statuts "lu" et "archivé" de deux courriels distincts, vous devez envoyer les éléments suivants:

PUT /emails
POSTDATA: [
            {
              id:1,
              to:"[email protected]",
              from:"[email protected]",
              subject:"Try this recipe!",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1t Mustard Powder",
              read:true,
              archived:true,
              importance:2,
              labels:["Someone","Mustard"]
            },
            {
              id:2,
              to:"[email protected]",
              from:"[email protected]",
              subject:"Try this recipe (With Fix)",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1T Mustard Powder, 1t Garlic Powder",
              read:true,
              archived:false,
              importance:1,
              labels:["Someone","Mustard"]
            }
            ]

Je séparerais les composants modifiables de l'e-mail (lecture, archivage, importance, étiquettes) en un objet séparé, car les autres (vers, depuis, sujet, texte) ne seraient jamais mis à jour.

PUT /email-statuses
POSTDATA: [
            {id:15,read:true,archived:true,importance:2,labels:["Someone","Mustard"]},
            {id:27,read:true,archived:false,importance:1,labels:["Someone","Mustard"]}
          ]

Une autre approche consiste à tirer parti de l’utilisation d’un PATCH. Indiquer explicitement quelles propriétés vous souhaitez mettre à jour et que toutes les autres doivent être ignorées.

PATCH /emails
POSTDATA: [
            {
              id:1,
              read:true,
              archived:true
            },
            {
              id:2,
              read:true,
              archived:false
            }
          ]

Les gens déclarent que PATCH devrait être mis en œuvre en fournissant un tableau de modifications contenant: action (CRUD), chemin (URL) et changement de valeur. Cela peut être considéré comme une implémentation standard, mais si vous regardez l'intégralité de l'API REST), il s'agit d'une API ponctuelle non intuitive. En outre, l'implémentation ci-dessus indique comment GitHub a PATCH mis en œuvre .

Pour résumer, il est possible d'adhérer aux principes RESTful avec des actions par lots tout en maintenant des performances acceptables.

11
justin.hughey

L'API Google Drive dispose d'un système très intéressant pour résoudre ce problème ( voir ici ).

Ce qu’ils font, c’est essentiellement de regrouper différentes demandes dans un Content-Type: multipart/mixed demande, chaque demande complète individuelle étant séparée par un délimiteur défini. Les en-têtes et les paramètres de requête de la requête batch sont hérités des requêtes individuelles (c'est-à-dire Authorization: Bearer some_token) sauf s’ils sont remplacés dans la requête individuelle.


Exemple : (extrait de leur docs )

Requête:

POST https://www.googleapis.com/batch

Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.20.0 (gzip)
Content-Type: multipart/mixed; boundary=END_OF_PART
Content-Length: 963

--END_OF_PART
Content-Length: 337
Content-Type: application/http
content-id: 1
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id
Authorization: Bearer authorization_token
Content-Length: 70
Content-Type: application/json; charset=UTF-8


{
  "emailAddress":"[email protected]",
  "role":"writer",
  "type":"user"
}
--END_OF_PART
Content-Length: 353
Content-Type: application/http
content-id: 2
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id&sendNotificationEmail=false
Authorization: Bearer authorization_token
Content-Length: 58
Content-Type: application/json; charset=UTF-8


{
  "domain":"appsrocks.com",
   "role":"reader",
   "type":"domain"
}
--END_OF_PART--

Réponse:

HTTP/1.1 200 OK
Alt-Svc: quic=":443"; p="1"; ma=604800
Server: GSE
Alternate-Protocol: 443:quic,p=1
X-Frame-Options: SAMEORIGIN
Content-Encoding: gzip
X-XSS-Protection: 1; mode=block
Content-Type: multipart/mixed; boundary=batch_6VIxXCQbJoQ_AATxy_GgFUk
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
Date: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Vary: X-Origin
Vary: Origin
Expires: Fri, 13 Nov 2015 19:28:59 GMT

--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-1


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "12218244892818058021i"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-2


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "04109509152946699072k"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk--
7
Aides

Je serais tenté par une opération telle que celle de votre exemple d'écrire un analyseur d'intervalle.

Ce n'est pas très ennuyeux de créer un analyseur pouvant lire "messageIds = 1-3,7-9,11,12-15". Cela augmenterait certainement l'efficacité des opérations générales couvrant tous les messages et serait plus évolutif.

1
One Monkey

Super article. Je cherche une solution depuis quelques jours. J'ai proposé une solution consistant à passer une chaîne de requête avec un groupe d'identifiants séparés par des virgules, comme:

DELETE /my/uri/to/delete?id=1,2,3,4,5

... puis en passant cela à un WHERE IN clause dans mon SQL. Cela fonctionne très bien, mais vous vous demandez ce que les autres pensent de cette approche.

1
Roberto

De mon point de vue, je pense que Facebook a la meilleure implémentation.

Une seule requête HTTP est faite avec un paramètre batch et un pour un jeton.

En lot un json est envoyé. qui contient une collection de "requêtes". Chaque demande a une propriété de méthode (get/post/put/delete/etc ...), et une propriété relative_url (uri du noeud final). De plus, les méthodes post et put permettent une propriété "body" où les champs doivent être mis à jour. sont envoyés .

plus d'infos sur: API de lot Facebook

0