Je construis une API générique avec un contenu et un schéma qui peut être défini par l'utilisateur. Je souhaite ajouter une logique de filtrage aux réponses de l'API afin que les utilisateurs puissent rechercher des objets spécifiques qu'ils ont stockés dans l'API. Par exemple, si un utilisateur stocke des objets d'événement, il peut effectuer des opérations telles que filtrer sur:
properties.categories
contient Engineering
properties.created_at
est plus ancien que 2016-10-02
properties.address.city
n'est pas Washington
properties.name
est Meetup
J'essaie de concevoir le filtrage dans la chaîne de requête des réponses d'API et de proposer quelques options, mais je ne suis pas sûr de la syntaxe la mieux adaptée ...
/events?properties.name=Harry&properties.address.city.neq=Washington
Cet exemple utilise uniquement un objet imbriqué pour spécifier les opérateurs (comme neq
comme indiqué). C'est bien en ce sens que c'est très simple et facile à lire.
Toutefois, dans les cas où les propriétés d'un événement peuvent être définies par l'utilisateur, il se produit un conflit potentiel entre une propriété nommée address.city.neq
utilisant un opérateur égal et normal et une propriété nommée address.city
utilisant un opérateur non égal.
Exemple: API de Stripe
/events?properties.name=Harry&properties.address.city+neq=Washington
Cet exemple est similaire au premier, à la différence qu’il utilise un délimiteur +
(qui équivaut à un espace) pour les opérations, au lieu de .
, afin d’éviter toute confusion, car les clés de mon domaine ne peuvent pas contenir d’espaces.
L’un des inconvénients est qu’il est un peu plus difficile à lire, bien que ce soit discutable car cela pourrait être interprété comme étant plus clair. Une autre pourrait être qu'il est légèrement plus difficile à analyser, mais pas tant que ça.
/events?properties.name=Harry&properties.address.city=neq:Washington
Cet exemple est très similaire au précédent, à ceci près qu'il déplace la syntaxe de l'opérateur dans la valeur du paramètre au lieu de la clé. Cela présente l'avantage d'éliminer un peu de la complexité liée à l'analyse de la chaîne de requête.
Mais cela se fait au prix de ne plus pouvoir différencier un opérateur égal vérifiant la chaîne littérale neq:Washington
et un opérateur différent de la chaîne Washington
.
Exemple: API Sparkpay
/events?filter=properties.name==Harry;properties.address.city!=Washington
Cet exemple utilise un seul paramètre de requête de niveau supérieur, filter
, pour nommer la totalité de la logique de filtrage sous. C'est bien en ce sens que vous n'avez jamais à vous soucier de la collision entre les espaces de noms de premier niveau. (Bien que dans mon cas, tout ce qui est personnalisé soit imbriqué sous properties.
, ce n'est donc pas un problème en premier lieu.)
Toutefois, cela nécessite une chaîne de requête plus difficile à taper lorsque vous souhaitez effectuer un filtrage de base des égalités, ce qui vous obligera probablement à consulter la documentation la plupart du temps. Et s’appuyer sur des symboles pour les opérateurs peut prêter à confusion pour des opérations non évidentes telles que "proche" ou "dans" ou "contient".
Exemple: API de Google Analytics
/events?filter=properties.name eq Harry; properties.address.city neq Washington
Cet exemple utilise un paramètre filter
de niveau supérieur similaire au paramètre précédent, mais il épelle les opérateurs avec Word au lieu de les définir avec des symboles, avec des espaces entre eux. Cela pourrait être légèrement plus lisible.
Mais cela a un coût: avoir une URL plus longue et beaucoup d'espaces qu'il faudra encoder?
Exemple: API OData
/events?filter[1][key]=properties.name&filter[1][eq]=Harry&filter[2][key]=properties.address.city&filter[2][neq]=Washington
Cet exemple utilise également un paramètre filter
de niveau supérieur, mais au lieu de créer une syntaxe entièrement personnalisée pour celui-ci qui imite la programmation, il construit plutôt une définition d'objet de filtres à l'aide d'une syntaxe de chaîne de requête plus standard. Cela a l'avantage d'apporter un peu plus de "standard".
Mais cela a le coût d'être très verbeux et difficile à analyser.
Exemple API de Magento
Étant donné tous ces exemples ou une approche différente, quelle syntaxe est la meilleure? Idéalement, il serait facile de construire le paramètre de requête afin de pouvoir jouer dans la barre d’URL tout en ne posant pas de problèmes d’interopérabilité future.
Je me penche vers # 2 car il semble que ce soit lisible, mais ne présente pas certains des inconvénients des autres schémas.
Je ne réponds peut-être pas à la question "Le meilleur choix", mais je peux au moins vous donner quelques idées et autres exemples à prendre en compte.
Tout d'abord, vous parlez d'une "API générique avec un contenu et un schéma pouvant être défini par l'utilisateur".
Cela ressemble beaucoup à solr / elasticsearch qui sont tous les deux des enveloppeurs de haut niveau sur Apache Lucene , qui indexe et agrège les documents.
Ces deux-là ont adopté une approche totalement différente de leur API de repos, il m'est arrivé de travailler avec eux deux.
Elasticsearch:
Query DSL, basé sur JSON, se présente comme suit:
GET /_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Search" }},
{ "match": { "content": "Elasticsearch" }}
],
"filter": [
{ "term": { "status": "published" }},
{ "range": { "publish_date": { "gte": "2015-01-01" }}}
]
}
}
}
Tiré de leur courant doc . J'ai été surpris que vous puissiez réellement mettre des données dansGET... Cela semble en fait mieux maintenant, dans les versions précédentes, c'était beaucoup plus - hiérarchique .
D'après mon expérience personnelle, cet ADSL était puissant, mais difficile à apprendre et à utiliser couramment (en particulier les versions plus anciennes). Et pour obtenir un résultat, vous avez besoin de plus que de jouer avec l'URL. En commençant par le fait que de nombreux clients ne prennent même pas en charge les données dans, OBTENEZrequest.
SOLR:
Ils ont tout mis dans les paramètres de requête, qui ressemblent en gros à ceci (tiré du doc ):
q=*:*&fq={!cache=false cost=5}inStock:true&fq={!frange l=1 u=4 cache=false cost=50}sqrt(popularity)
Travailler avec cela était plus simple. Mais ce n'est que mon goût personnel.
Maintenant à propos de mes expériences. Nous implémentions une autre couche au-dessus de ces deux et nous avons pris l'approche numéro # 4. En fait, je pense que # 4 et # 5 devraient être pris en charge en même temps. Pourquoi? Parce que quoi que vous choisissiez, les gens vont se plaindre, et puisque de toute façon vous aurez votre propre "micro-DSL", vous pouvez également prendre en charge quelques alias de plus pour vos mots-clés.
Pourquoi pas # 2 ? Avoir un paramètre de filtre unique et une requête à l'intérieur vous donne un contrôle total sur DSL. Six mois après la création de notre ressource, nous avons reçu une "simple" demande de fonctionnalité: un nom logique OR
et une parenthèse ()
. Les paramètres de requête sont à la base une liste d'opérations AND
et d'ordonnances logiques OR
telles que city=London OR age>25
ne correspondent pas vraiment à cela. Par ailleurs, la parenthèse introduit l’emboîtement dans la structure DSL, ce qui poserait également un problème dans la structure de chaîne de requête horizontale.
Eh bien, c’est là les problèmes sur lesquels nous sommes tombés, votre cas pourrait être différent. Mais il reste à considérer quelles seront les attentes futures de cette API.
J'aime l'aspectde Google AnalyticsAPI de filtre, facile à utiliser et à comprendre du point de vue du client.
Ils utilisent un formulaire encodé en URL, par exemple:
- Égal à :% 3D% 3D
filters=ga:timeOnPage%3D%3D10
- Pas égal :!% 3D
filters=ga:timeOnPage!%3D10
Bien que vous ayez besoin de vérifier la documentation, celle-ci a tout de même ses avantages. Si vous pensez que les utilisateurs peuvent s'y habituer, alors foncez.
Utiliser des opérateurs comme suffixes clés semble également une bonne idée (selon vos besoins).
Cependant, je recommanderais d'encoder le signe +
de manière à ce qu'il ne soit pas analysé comme un space
. En outre, il pourrait être un peu plus difficile d'analyser comme mentionné, mais je pense que vous pouvez écrire un analyseur personnalisé pour celui-ci. Je suis tombé sur this Gist by jlong il y a quelque temps. Peut-être trouverez-vous utile d'écrire votre analyseur.
Vous pouvez également essayer Spring Expression Language (SpEL)
Tout ce que vous avez à faire est de vous en tenir au format indiqué dans le document. Le moteur SpEL se chargera d’analyser la requête et de l’exécuter sur un objet donné. Semblable à votre exigence de filtrer une liste d'objets, vous pouvez écrire la requête en tant que:
properties.address.city == 'Washington' and properties.name == 'Harry'
Il prend en charge tous les types d'opérateurs logiques et relationnels dont vous auriez besoin. L'API restante pourrait simplement prendre cette requête en tant que chaîne de filtrage et la transmettre au moteur SpEL pour qu'elle s'exécute sur un objet.
Avantages: lisible, facile à écrire et bien exécuté.
Donc, l'URL ressemblerait à ceci:
/events?filter="properties.address.city == 'Washington' and properties.name == 'Harry'"
Exemple de code utilisant org.springframework: spring-core: 4.3.4.RELEASE:
La fonction principale d'intérêt:
/**
* Filter the list of objects based on the given query
*
* @param query
* @param objects
* @return
*/
private static <T> List<T> filter(String query, List<T> objects) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(query);
return objects.stream().filter(obj -> {
return exp.getValue(obj, Boolean.class);
}).collect(Collectors.toList());
}
Exemple complet avec des classes d’aide et d’autres codes non intéressants:
import Java.util.Arrays;
import Java.util.List;
import Java.util.stream.Collectors;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class SpELTest {
public static void main(String[] args) {
String query = "address.city == 'Washington' and name == 'Harry'";
Event event1 = new Event(new Address("Washington"), "Harry");
Event event2 = new Event(new Address("XYZ"), "Harry");
List<Event> events = Arrays.asList(event1, event2);
List<Event> filteredEvents = filter(query, events);
System.out.println(filteredEvents.size()); // 1
}
/**
* Filter the list of objects based on the query
*
* @param query
* @param objects
* @return
*/
private static <T> List<T> filter(String query, List<T> objects) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(query);
return objects.stream().filter(obj -> {
return exp.getValue(obj, Boolean.class);
}).collect(Collectors.toList());
}
public static class Event {
private Address address;
private String name;
public Event(Address address, String name) {
this.address = address;
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static class Address {
private String city;
public Address(String city) {
this.city = city;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
}
J'ai décidé de comparer les approches # 1/# 2 (1) et # 3 (2) et j'ai conclu que (1) est préférable (du moins pour le côté serveur Java).
Supposons que certains paramètres a
doivent être égaux à 10 ou 20. Dans ce cas, notre requête d’URL doit ressembler à ?a.eq=10&a.eq=20
pour (1) et ?a=eq:10&a=eq:20
pour (2). En Java, HttpServletRequest#getParameterMap()
renverra les valeurs suivantes: { a.eq: [10, 20] }
pour (1) et { a: [eq:10, eq:20] }
pour (2). Par la suite, nous devons convertir les mappes renvoyées, par exemple, en clause SQL where
. Et nous devrions obtenir: where a = 10 or a = 20
pour les deux (1) et (2). En bref, cela ressemble à quelque chose comme ça:
1) ?a=eq:10&a=eq:20 -> { a: [eq:10, eq:20] } -> where a = 10 or a = 20
2) ?a.eq=10&a.eq=20 -> { a.eq: [10, 20] } -> where a = 10 or a = 20
Nous avons donc la règle suivante: Lorsque nous passons à la requête d'URL deux paramètres portant le même nom, nous devons utiliser l'opérande OR
dans SQL .
Mais supposons un autre cas. Le paramètre a
doit être supérieur à 10 et inférieur à 20. En appliquant la règle ci-dessus, nous aurons la conversion suivante:
1) ?a.gt=10&a.ls=20 -> { a.gt: 10, a.lt: 20 } -> where a > 10 and a < 20
2) ?a=gt:10&a=ls:20 -> { a: [gt.10, lt.20] } -> where a > 10 or(?!) a < 20
Comme vous pouvez le voir, dans (1) nous avons deux paramètres avec différent noms: a.gt
et a.ls
. Cela signifie que notre requête SQL aura l'opérande AND
. Mais pour (2) nous avons toujours les mêmes noms et il doit être converti en SQL avec l'opérande OR
!
Cela signifie que pour (2) au lieu d'utiliser #getParameterMap()
, nous devons analyser directement la requête URL et analyser les noms de paramètres répétés.
Je sais que c'est de la vieille école, mais qu'en est-il d'une sorte de surcharge d'opérateur?
Cela rendrait la requête beaucoup plus difficile à analyser (et non CGI standard), mais ressemblerait au contenu d'une clause SQL WHERE.
/events?properties.name=Harry&properties.address.city+neq=Washington
deviendrait
/events?properties.name== 'Harry'&&properties.address.city!='Washington'||properties.name==' Jack 'et&properties.address.city!=('Paris', 'Nouvelle-Orléans')
la paranthesis commencerait une liste. Garder les chaînes entre guillemets simplifierait l'analyse.
La requête ci-dessus concerne donc les événements pour Harry n’est pas à Washington ou pour les Jacks pas à Paris ou à La Nouvelle-Orléans.
Ce serait une tonne de travail à mettre en œuvre ... et l'optimisation de la base de données pour exécuter ces requêtes serait un cauchemar, mais si vous recherchez un langage de requête simple et puissant, il suffit d'imiter le code SQL :)
-k