Comment concevoir une recherche / filtrage RESTful? [fermé]

457

Je conçois et implémente actuellement une API RESTful en PHP. Cependant, je n'ai pas réussi à implémenter ma conception initiale.

GET /users # list of users
GET /user/1 # get user with id 1
POST /user # create new user
PUT /user/1 # modify user with id 1
DELETE /user/1 # delete user with id 1

Jusqu'à présent assez standard, non?

Mon problème est avec le premier GET /users. J'envisageais d'envoyer des paramètres dans le corps de la demande pour filtrer la liste. C'est parce que je veux pouvoir spécifier des filtres complexes sans avoir une URL super longue, comme:

GET /users?parameter1=value1&parameter2=value2&parameter3=value3&parameter4=value4

Au lieu de cela, je voulais avoir quelque chose comme:

GET /users
# Request body:
{
    "parameter1": "value1",
    "parameter2": "value2",
    "parameter3": "value3",
    "parameter4": "value4"
}

qui est beaucoup plus lisible et vous offre de grandes possibilités pour définir des filtres complexes.

Quoi qu'il en soit, file_get_contents('php://input')n'a pas renvoyé le corps de la demande pour les GETdemandes. J'ai également essayé http_get_request_body(), mais l'hébergement partagé que j'utilise n'a pas pecl_http. Pas sûr que cela aurait aidé de toute façon.

J'ai trouvé cette question et j'ai réalisé que GET n'est probablement pas censé avoir un corps de demande. C'était un peu peu concluant, mais ils l'ont déconseillé.

Alors maintenant, je ne sais pas quoi faire. Comment concevez-vous une fonction de recherche / filtrage RESTful?

Je suppose que je pourrais utiliser POST, mais cela ne semble pas très reposant.

Erik B
la source
7
copie possible de la conception d'URL RESTful pour la recherche
outis
60
Faites attention!!! La méthode GET doit être IDEMPOTENT et doit être "cacheable". Si vous envoyez des informations dans le corps Comment le système peut-il mettre en cache votre demande? HTTP permet de mettre en cache la requête GET en utilisant uniquement l'URL, pas le corps de la requête. Par exemple, ces deux requêtes: example.com {test: "some"} example.com {anotherTest: "some2"} sont considérées comme identiques par le système de cache: les deux ont exactement la même URL
jfcorugedo
15
Juste pour ajouter, vous devriez POSTER sur / users (collection) et non / user (single user).
Mladen B.
1
Un autre point à considérer est que la plupart des serveurs d'applications ont des journaux d'accès qui enregistrent l'URL et peuvent donc être quelque chose entre les deux. Il peut donc y avoir une fuite d'informations non voulue sur GET.
user3206144
2
Copie

Réponses:

396

La meilleure façon d'implémenter une recherche RESTful est de considérer la recherche elle-même comme une ressource. Ensuite, vous pouvez utiliser le verbe POST car vous créez une recherche. Vous n'avez pas besoin de créer littéralement quelque chose dans une base de données pour utiliser un POST.

Par exemple:

Accept: application/json
Content-Type: application/json
POST http://example.com/people/searches
{
  "terms": {
    "ssn": "123456789"
  },
  "order": { ... },
  ...
}

Vous créez une recherche du point de vue de l'utilisateur. Les détails de mise en œuvre de cela ne sont pas pertinents. Certaines API RESTful peuvent même ne pas avoir besoin de persistance. C'est un détail d'implémentation.

Jason Harrelson
la source
209
Une limitation importante de l'utilisation d'une demande POST pour un point de terminaison de recherche est qu'elle ne peut pas être mise en signet. La mise en signet des résultats de recherche (en particulier les requêtes complexes) peut être très utile.
couchand
73
L'utilisation de POST pour effectuer des recherches peut briser la contrainte de cache REST. whatisrest.com/rest_constraints/cache_excerps
Filipe
56
Les recherches, de par leur nature, sont transitoires: les données évoluent entre deux recherches avec les mêmes paramètres, donc je pense qu'une requête GET ne correspond pas correctement au modèle de recherche. Au lieu de cela, la demande de recherche doit être POST (/ Resource / search), alors vous pouvez enregistrer cette recherche et la rediriger vers un résultat de recherche, par exemple / Resource / search / iyn3zrt. De cette façon, les demandes GET réussissent et ont du sens.
sleblanc
32
Je ne pense pas que la publication soit une méthode appropriée pour la recherche, les données pour les demandes GET normales peuvent également varier dans le temps.
me demande
82
C'est absolument la pire réponse possible. Je ne peux pas croire qu'il ait autant de votes positifs. Cette réponse explique pourquoi: programmers.stackexchange.com/questions/233164/…
richard
141

Si vous utilisez le corps de la demande dans une demande GET, vous ne respectez pas le principe REST, car votre demande GET ne pourra pas être mise en cache, car le système de cache utilise uniquement l'URL.

Et pire encore, votre URL ne peut pas être mise en signet, car l'URL ne contient pas toutes les informations nécessaires pour rediriger l'utilisateur vers cette page

Utilisez des paramètres d'URL ou de requête au lieu des paramètres de corps de demande.

par exemple:

/myapp?var1=xxxx&var2=xxxx
/myapp;var1=xxxx/resource;var2=xxxx 

En fait, le HTTP RFC 7231 dit que:

Une charge utile dans un message de demande GET n'a pas de sémantique définie; l'envoi d'un corps de charge utile sur une demande GET peut entraîner le rejet de la demande par certaines implémentations existantes.

Pour plus d'informations jetez un œil ici

jfcorugedo
la source
29
Apprenez de mon erreur - j'ai conçu une API en utilisant la suggestion de la réponse acceptée (POSTing json), mais je passe aux paramètres d'URL. La capacité de signet peut être plus importante que vous ne le pensez. Dans mon cas, il était nécessaire de diriger le trafic vers certaines requêtes de recherche (campagne publicitaire). En outre, l'utilisation de l'API historique est plus logique avec les paramètres d'URL.
Jake
2
Cela dépend de la façon dont il est utilisé. Si vous créez un lien vers une URL qui charge la page en fonction de ces paramètres, cela a du sens, mais si la page principale effectue un appel AJAX uniquement pour obtenir les données en fonction des paramètres de filtre, vous ne pouvez pas le marquer de toute façon car c'est un ajax appel et n'a aucune incidence. Naturellement, vous pouvez également mettre en signet une URL qui, lorsque vous y allez, crée un filtre et des messages POST pour l'appel ajax et cela fonctionnerait très bien.
Daniel Lorenz
@DanielLorenz Pour la meilleure expérience utilisateur, l'URL doit toujours être modifiée via l'API Historique dans ce cas. Je ne peux pas rester debout lorsqu'un site Web ne permet pas d'utiliser la fonctionnalité de retour du navigateur pour naviguer vers les pages précédentes. Et s'il s'agit d'une page générée côté serveur standard, la seule façon de la rendre signet serait d'utiliser une requête GET. Il semble que de bons vieux paramètres de requête soient la meilleure solution.
Nathan
@Nathan Je pense que j'ai mal lu cette réponse. Je parlais d'utiliser des paramètres de chaîne de requête dans un get. Vous ne devez jamais utiliser de paramètres de corps dans un appel GET car cela serait complètement inutile. Je parlais plus d'un GET avec une chaîne de requête pouvant être utilisée / mise en signet, puis au démarrage de la page, vous pouvez utiliser ces paramètres pour créer un filtre pour POST, en utilisant ces paramètres pour obtenir les données. L'histoire fonctionnerait toujours bien dans ce scénario.
Daniel Lorenz
@DanielLorenz Ah d'accord ça a du sens. Je pense que j'ai mal compris ce que vous disiez.
Nathan
70

Il semble que le filtrage / recherche de ressources puisse être implémenté de manière RESTful. L'idée est d'introduire un nouveau noeud final appelé /filters/ou /api/filters/.

L'utilisation de ce filtre de point de terminaison peut être considérée comme une ressource et donc créée via une POSTméthode. De cette façon - bien sûr - le corps peut être utilisé pour transporter tous les paramètres ainsi que des structures de recherche / filtre complexes peuvent être créées.

Après avoir créé un tel filtre, il existe deux possibilités pour obtenir le résultat de la recherche / du filtre.

  1. Une nouvelle ressource avec un ID unique sera retournée avec le 201 Createdcode d'état. Ensuite, en utilisant cet ID, une GETdemande peut être faite pour /api/users/aimer:

    GET /api/users/?filterId=1234-abcd
    
  2. Une fois le nouveau filtre créé via, POSTil ne répondra pas avec 201 Createdmais immédiatement avec l' 303 SeeOtheren- Locationtête pointant vers /api/users/?filterId=1234-abcd. Cette redirection sera automatiquement gérée via la bibliothèque sous-jacente.

Dans les deux scénarios, deux demandes doivent être faites pour obtenir les résultats filtrés - cela peut être considéré comme un inconvénient, en particulier pour les applications mobiles. Pour les applications mobiles, j'utiliserais un seul POSTappel à /api/users/filter/.

Comment conserver les filtres créés?

Ils peuvent être stockés dans DB et utilisés plus tard. Ils peuvent également être stockés dans un stockage temporaire, par exemple redis et avoir un TTL après lequel ils expireront et seront supprimés.

Quels sont les avantages de cette idée?

Les filtres, les résultats filtrés peuvent être mis en cache et peuvent même être mis en signet.

Opale
la source
2
Eh bien, cela devrait être la réponse acceptée. Vous ne violez pas les principes REST et vous pouvez effectuer de longues requêtes complexes sur les ressources. Il est agréable, propre et compatible avec les signets. Le seul inconvénient supplémentaire est la nécessité de stocker des paires clé / valeur pour les filtres créés et les deux étapes de demande déjà mentionnées.
dantebarba
2
Le seul problème avec cette approche est, si vous avez des filtres date-heure dans la requête (ou une valeur en constante évolution). Le nombre de filtres à stocker dans db (ou cache) est alors innombrable.
Rvy Pandey
17

Je pense que vous devriez utiliser les paramètres de demande, mais seulement tant qu'il n'y a pas d'en-tête HTTP approprié pour accomplir ce que vous voulez faire. La spécification HTTP ne dit pas explicitement que GET ne peut pas avoir de corps. Cependant, ce document déclare:

Par convention, lorsque la méthode GET est utilisée, toutes les informations nécessaires pour identifier la ressource sont codées dans l'URI. Il n'y a pas de convention dans HTTP / 1.1 pour une interaction sûre (par exemple, récupération) où le client fournit des données au serveur dans un corps d'entité HTTP plutôt que dans la partie requête d'un URI. Cela signifie que pour des opérations sûres, les URI peuvent être longs.

Daff
la source
6
ElasticSearch fait également GET avec le corps et fonctionne bien!
Tarun Sapra
Oui mais ils contrôlent l'implémentation du serveur peut-être pas sur les interwebs.
user432024
7

Comme j'utilise un backend laravel / php, j'ai tendance à aller avec quelque chose comme ça:

/resource?filters[status_id]=1&filters[city]=Sydney&page=2&include=relatedResource

PHP transforme automatiquement les []paramètres en tableau, donc dans cet exemple, je vais me retrouver avec une $filtervariable qui contient un tableau / objet de filtres, ainsi qu'une page et toutes les ressources connexes que je veux ardemment chargées.

Si vous utilisez une autre langue, cela peut toujours être une bonne convention et vous pouvez créer un analyseur à convertir []en tableau.

the-a-train
la source
Cette approche a l'air sympa, mais il pourrait y avoir des problèmes avec l'utilisation de crochets dans les URL, voir what-characters-can-one-use-in-a-url
Sky
2
@Sky Cela pourrait être évité en codant l'URI des [et ]. L'utilisation de représentations codées de ces caractères pour regrouper les paramètres de requête est une pratique bien connue. Il est même utilisé dans la spécification JSON: API .
jelhan
6

Ne vous inquiétez pas trop si votre API initiale est entièrement RESTful ou non (surtout lorsque vous êtes juste au stade alpha). Faites fonctionner la plomberie principale en premier. Vous pouvez toujours faire une sorte de transformation / réécriture d'URL pour mapper les choses, en affinant de manière itérative jusqu'à ce que vous obteniez quelque chose d'assez stable pour des tests généralisés ("beta").

Vous pouvez définir des URI dont les paramètres sont codés par position et convention sur les URI eux-mêmes, préfixés par un chemin que vous savez que vous mapperez toujours à quelque chose. Je ne connais pas PHP, mais je suppose qu'une telle fonctionnalité existe (comme elle existe dans d'autres langages avec des frameworks web):

.c'est à dire. Effectuez une recherche de type "utilisateur" avec param [i] = valeur [i] pour i = 1..4 sur le magasin # 1 (avec valeur1, valeur2, valeur3, ... comme raccourci pour les paramètres de requête URI):

1) GET /store1/search/user/value1,value2,value3,value4

ou

2) GET /store1/search/user,value1,value2,value3,value4

ou comme suit (bien que je ne le recommanderais pas, plus à ce sujet plus tard)

3) GET /search/store1,user,value1,value2,value3,value4

Avec l'option 1, vous mappez tous les URI préfixés avec /store1/search/userle gestionnaire de recherche (ou la désignation PHP) par défaut pour effectuer des recherches de ressources sous store1 (équivalent à /search?location=store1&type=user.

Par convention documentée et appliquée par l'API, les valeurs des paramètres 1 à 4 sont séparées par des virgules et présentées dans cet ordre.

L'option 2 ajoute le type de recherche (dans ce cas user) comme paramètre positionnel # 1. L'une ou l'autre option n'est qu'un choix esthétique.

L'option 3 est également possible, mais je ne pense pas que je l'aimerais. Je pense que la capacité de recherche dans certaines ressources devrait être présentée dans l'URI lui-même précédant la recherche elle-même (comme si elle indiquait clairement dans l'URI que la recherche est spécifique au sein de la ressource.)

L'avantage de cela sur le passage de paramètres sur l'URI est que la recherche fait partie de l'URI (traitant ainsi une recherche comme une ressource, une ressource dont le contenu peut - et changera - au fil du temps.) L'inconvénient est que l'ordre des paramètres est obligatoire .

Une fois que vous avez fait quelque chose comme ça, vous pouvez utiliser GET, et ce serait une ressource en lecture seule (puisque vous ne pouvez pas POST ou PUT - il est mis à jour quand il est GET). Ce serait également une ressource qui n'existerait que lorsqu'elle serait invoquée.

On pourrait également y ajouter plus de sémantique en mettant en cache les résultats pendant un certain temps ou avec une SUPPRESSION entraînant la suppression du cache. Cependant, cela peut aller à l'encontre de ce que les gens utilisent généralement pour SUPPRIMER (et parce que les gens contrôlent généralement la mise en cache avec des en-têtes de mise en cache.)

La façon dont vous allez procéder serait une décision de conception, mais ce serait la façon dont je procéderais. Ce n'est pas parfait, et je suis sûr qu'il y aura des cas où ce n'est pas la meilleure chose à faire (spécialement pour des critères de recherche très complexes).

luis.espinal
la source
7
Yo, si vous (quelqu'un, qui que ce soit / quoi que ce soit) approprite pour déprécier ma réponse, cela vous nuirait-il à votre ego de mettre au moins un commentaire indiquant exactement avec quoi vous êtes en désaccord? Je sais que c'est l'interweebz, mais ...;)
luis.espinal
107
Je n'ai pas downvote, mais le fait que la question commence par: "Je suis en train de concevoir et d'implémenter une API RESTful" et votre réponse commence par "Ne vous inquiétez pas trop si votre API initiale est entièrement RESTful ou non" mal pour moi. Si vous concevez une API, vous concevez une API. La question est de savoir comment concevoir au mieux l'API, et non pas si l'API doit être conçue.
gardarh
14
L'API est le système, travaillez d'abord sur l'API, pas sur la plomberie principale, la première implémentation pourrait / devrait être juste une maquette. HTTP a un mécanisme pour passer des paramètres, vous proposez qu'il soit réinventé, mais pire (paramètres ordonnés au lieu de paires de valeurs de nom). D'où le vote négatif.
Steven Herod
14
@gardarh - oui, ça ne va pas, mais parfois, c'est pragmatique. L'objectif principal est de concevoir une API qui fonctionne pour le contexte commercial en question. Si une approche entièrement RESTFULL est appropriée pour l'entreprise en question, alors allez-y. Si ce n'est pas le cas, n'allez pas de l'avant. Autrement dit, concevez une API qui répond à vos besoins commerciaux spécifiques. Faire le tour en essayant de le rendre RESTfull comme sa principale exigence n'est pas très différent de demander "comment utiliser le modèle d'adaptateur dans le problème X / Y". Ne sabrez pas les paradigmes de la corne à moins qu'ils ne résolvent des problèmes réels et précieux.
luis.espinal
1
Je considère une ressource comme une collection d'états et des paramètres comme un moyen de manipuler la représentation de cet état de façon paramétrique. Pensez-y de cette façon, si vous pouviez utiliser des boutons et des commutateurs pour ajuster la façon dont la ressource est affichée (afficher / masquer certains morceaux de celle-ci, la classer différemment, etc ...) ces contrôles sont des paramètres. S'il s'agit en fait d'une ressource différente ('/ albums' vs '/ artistes', par exemple), c'est alors qu'elle devrait être représentée dans le chemin. C'est ce qui m'est intuitif de toute façon.
Eric Elliott
2

FYI: Je sais que c'est un peu tard mais pour tous ceux qui sont intéressés. Cela dépend de la façon dont vous voulez être RESTful, vous devrez implémenter vos propres stratégies de filtrage car la spécification HTTP n'est pas très claire à ce sujet. Je voudrais suggérer de coder par URL tous les paramètres de filtre, par exemple

GET api/users?filter=param1%3Dvalue1%26param2%3Dvalue2

Je sais que c'est moche mais je pense que c'est la façon la plus reposante de le faire et devrait être facile à analyser côté serveur :)

jarrets
la source