web-dev-qa-db-fra.com

API RESTful et opérations en bloc

J'ai un niveau intermédiaire qui effectue des opérations CRUD sur une base de données partagée. Lorsque j'ai converti le produit en .NET Core, je pensais aussi que j'utiliserais REST pour l'API, car CRUD est censé être ce qu'il fait bien. Il semble que REST soit une excellente solution pour les opérations d'enregistrement unique, mais que se passe-t-il lorsque je souhaite supprimer, par exemple, 1 000 enregistrements?

Toutes les applications professionnelles multi-utilisateurs vont avoir un concept de vérification de concurrence optimiste: vous ne pouvez pas demander à un utilisateur d'effacer le travail d'un autre utilisateur sans un retour. Si j'ai bien compris, REST gère cela avec l'enregistrement d'en-tête HTTP ETag. Si l'ETag envoyé par le client ne correspond pas à la balise du serveur, vous émettez un 412 Precondition Failed . Jusqu'ici tout va bien. Mais que dois-je utiliser lorsque je veux supprimer 1 000 enregistrements? Le temps aller-retour pour 1 000 appels individuels est considérable, alors comment REST pourrait-il gérer une opération de traitement par lots impliquant Concurrence optimale ?

5
Donald Airey

REST se concentre sur les ressources et le découplage des clients des serveurs. Il ne s’agit pas simplement d’une architecture ou d’un protocole CRUD. Bien que CRUD et REST semblent être très similaires, la gestion des ressources via REST principes peut souvent aussi avoir des effets secondaires . Par conséquent, décrire REST comme une simple chose CRUD est une simplification excessive.

En ce qui concerne le traitement par lots de REST ressources, le protocole sous-jacent (le plus souvent HTTP) définit les fonctionnalités pouvant être utilisées. HTTP définit quelques opérations pouvant être utilisées pour modifier plusieurs ressources.

POST est le couteau universel du protocole et peut être utilisé pour gérer littéralement les ressources qui vous plaisent. Comme la sémantique est définie par le développeur, vous pouvez l’utiliser pour créer, mettre à jour ou supprimer plusieurs ressources à la fois.

PUT a la sémantique de remplacer l'état d'une ressource pouvant être obtenue à un URI donné par le corps de charge utile de la demande. Si vous envoyez une demande PUT à une ressource "list" et que la charge utile définit une liste d'entrées, vous pouvez également effectuer une opération par lots.

La différence fondamentale entre les méthodes POST et PUT est mise en évidence par l'intention différente de la représentation incluse. La ressource cible dans une demande POST est destinée à gérer la représentation incluse en fonction de la sémantique de la ressource, alors que la représentation incluse dans une demande PUT est définie comme remplaçant l'état de la ressource cible.

...

Une demande PUT appliquée à la ressource cible peut avoir des effets secondaires sur d'autres ressources. Par exemple, un article peut avoir un URI identifiant "la version actuelle" (une ressource) distincte des URI identifiant chaque version particulière (différentes ressources partageant le même état à un moment donné que la version actuelle). Une requête PUT réussie sur l'URI "de la version actuelle" pourrait par conséquent créer une nouvelle ressource de version en plus de changer l'état de la ressource cible et pourrait également entraîner l'ajout de liens entre les ressources associées. ( La source )

PATCH ( RFC 5789 ) n'est pas encore inclus dans le protocole HTTP, bien qu'il soit pris en charge par de nombreux frameworks. Il est principalement utilisé pour modifier plusieurs ressources à la fois ou pour effectuer des mises à jour partielles de ressources que PUT est également en mesure de réaliser si la partie mise à jour est une sous-ressource d'une autre ressource; dans ce cas, cela a pour effet une mise à jour partielle sur la ressource externe.

Il est important de savoir qu'une requête PATCH contient les étapes nécessaires qu'un serveur doit accomplir pour transformer une ressource en son état souhaité. Un client doit donc saisir l'état actuel et calculer au préalable les étapes nécessaires à la transformation. Un article de blog très informatif sur ce sujet est Ne pas patcher comme un idiot . Ici JSON Patch ( RFC ) est un type de support basé sur JSON qui visualise clairement le concept PATCH. Une demande de correctif doit être appliquée intégralement (chaque opération définie dans la demande de correctif) ou ne pas être appliquée du tout. Elle nécessite donc une gestion de la portée de la transaction et une restauration en cas d'échec de l'une des opérations.

Les requêtes conditionnelles telles que les en-têtes ETag et IfModifiedSince sont définies dans RFC 7232 et peuvent être utilisées dans les requêtes HTTP pour effectuer les modifications uniquement si la requête est appliquée à la version la plus récente de la ressource et correspond donc à un verrouillage optimiste (distribué). ) bases de données.

Jusqu'ici tout va bien. Mais que dois-je utiliser lorsque je veux supprimer 1 000 enregistrements?

Cela dépend du cadre que vous utiliserez. S'il accepte PATCH, je vote clairement pour PATCH. Dans le cas contraire, vous êtes probablement plus sûr d'utiliser POST que PUT à partir de la sémantique très restrictive que PUT a, car la sémantique est clairement définie par vous. Dans le cas d'une suppression par lot, vous pouvez également utiliser PUT en ciblant la ressource de collection avec un corps vide, ce qui a pour effet de supprimer tous les éléments de la collection et, par conséquent, d'effacer toute la collection. Toutefois, si certains éléments doivent rester dans la collection, PATCH ou POST est probablement plus facile à utiliser.

4
Roman Vottner

Si je comprends bien, vous voulez une concurrence optimiste pour chaque enregistrement individuellement. Autrement dit, chaque enregistrement ne doit être supprimé que si son état correspond aux attentes du client. (Si vous souhaitez uniquement déclarer l’état de la collection entière, alors If-Match et 412 suffisent.)

La réponse de Roman Vottner explique très bien les méthodes HTTP impliquées, mais j’essaierai de préciser certains détails.

Caveat emptor

Quand nous parlons de «comment REST gérera-t-il» ceci ou cela, vous comprenez que techniquement, vous pouvez utiliser HTTP comme transport pour toute opération de la manière qui vous convient.

Ainsi, lorsque vous parlez de REST, je suppose que vous êtes intéressé par une interface uniforme , une approche qui pourrait théoriquement être utilisée par divers clients et serveurs.

Mais le mot clé est “théoriquement”. Par exemple, une fois que vous définissez votre propre type de support (votre propre structure JSON), une grande partie de l’uniformité disparaît, puisqu'un client doit de toute façon être codé par rapport à votre API spécifique. Vous pouvez alors lui demander de sauter. à travers les cerceaux que vous voulez.

Mais si vous souhaitez toujours préserver autant que possible l'uniformité, lisez la suite.

Tout ou rien

Si vous souhaitez une opération tout-ou-rien, qui échoue entièrement en cas d'échec de l'une des conditions préalables, vous pouvez utiliser comme le suggère Roman PATCH avec le JSON Patch format. Pour cela, vous avez besoin d'une représentation conceptuelle de votre collection sous la forme d'un seul objet JSON, auquel le correctif doit être appliqué.

Par exemple, supposons que vous ayez des ressources telles que /my/collection/1, /my/collection/4, etc. Vous pourriez représenter /my/collection/ par:

{
    "resources": {
        "1": {
            "href": "1",
            "etag": "\"BRkDVtYw\"",
            "name": "Foo Bar",
            "price": 1234.5,
            ...
        },
        "4": {
            "href": "4",
            "etag": "\"RCi8knuN\"",
            "name": "Baz Qux",
            "price": 2345.6,
            ...
        },
        ...
    }
}

Ici, "1" et "4" sont des URL relatives à /my/collection/. Vous pouvez utiliser à la place des identifiants spécifiques à un domaine, mais le bon REST fonctionne en termes d'URL opaques.

Les normes n’exigent pas que vous signifiiez cette représentation sur GET /my/collection/, mais si vous acceptez une telle demande, vous devez utiliser cette représentation. Dans tous les cas, vous pouvez appliquer le correctif JSON suivant à cette structure:

PATCH /my/collection/ HTTP/1.1
Content-Type: application/json-patch+json

[
    {"op": "test", "path": "/resources/1/etag", "value": "\"BRkDVtYw\""},
    {"op": "remove", "path": "/resources/1"},
    {"op": "test", "path": "/resources/4/etag", "value": "\"RCi8knuN\""},
    {"op": "remove", "path": "/resources/4"},
    ...
]

path n’est pas un chemin d’URL, c’est un pointeur JSON dans la représentation ci-dessus.

Si toutes les opérations de correctif aboutissent, vous répondez avec un code de statut réussi, tel que 204 (pas de contenu) } ou 200 (OK) .

Si l’une des opérations ETag test échoue, vous répondez avec 409 (conflit) . Vous ne devriez pas répondre avec 412 (la condition préalable a échoué) } dans ce cas, car il n'y a aucune condition préalable (telle que If-Match) sur la demande elle-même.

Si quelque chose ne va pas, vous répondez avec les autres codes d'état appropriés: voir RFC 5789 § 2.2 et RFC 7231 § 6.6 .

Résultat mixte

Si vous ne voulez pas de sémantique «tout ou rien», je ne connais aucune solution standardisée. Comme Roman note, vous ne pouvez pas utiliser la méthode PATCH dans ce cas, mais vous pouvez utiliser POST avec un type de support personnalisé ( RFC 6838 § 3.4 ). Cela pourrait ressembler à ceci:

POST /my/collection/ HTTP/1.1
Content-Type: application/x.my-patch+json
Accept: application/x.my-patch-results+json

{
    "delete": [
        {"href": "1", "if-match": "\"BRkDVtYw\""},
        {"href": "4", "if-match": "\"RCi8knuN\""},
        ...
    ]
}

Vous pouvez répondre à une telle demande avec 200 (OK), que les suppressions individuelles aient réussi ou non. Une autre option serait (Multi-Status) } _, mais je ne vois aucun avantage à cela dans ce cas, et il n'est pas largement utilisé en dehors de WebDAV, donc la loi de Postel suggère de ne pas y aller.

HTTP/1.1 200 OK
Content-Type: application/x.my-patch-results+json

{
    "delete": [
        {"href": "1", "success": true},
        {"href": "4", "success": false, "error": {...}},
        ...
    ]
}

Bien sûr, si le correctif n'était pas valide, vous devez plutôt répondre avec 415 (type de support non pris en charge) } ou 422 (entité non traitable)) selon le cas.

Un autre angle

Le temps aller et retour pour 1000 appels individuels est considérable

C'est dans HTTP/1.1. Mais si vous pouvez utiliser HTTP/2 - qui prend beaucoup mieux en charge les demandes simultanées, ainsi que les frais généraux de réseau beaucoup plus réduits par demande -, 1 000 demandes individuelles peuvent alors parfaitement fonctionner.

0
Vasiliy Faronov