Langage de requête REST avec RSQL

Haut REST

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> DÉCOUVREZ LE COURS Haut Persistence

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS Cet article fait partie d'une série: • Langage de requête REST avec critères Spring et JPA

• Langage de requête REST avec spécifications Spring Data JPA

• Langage de requête REST avec Spring Data JPA et Querydsl

• REST Query Language - Opérations de recherche avancées

• REST Query Language - Implémentation de l'opération OR

• REST Query Language avec RSQL (article actuel) • REST Query Language avec Querydsl Web Support

1. Vue d'ensemble

Dans ce cinquième article de la série, nous allons illustrer la construction du langage de requête d'API REST à l'aide d' une bibliothèque sympa - rsql-parser.

RSQL est un super-ensemble du langage FIQL (Feed Item Query Language) - une syntaxe de filtre propre et simple pour les flux; il s'intègre donc tout naturellement dans une API REST.

2. Préparatifs

Tout d'abord, ajoutons une dépendance maven à la bibliothèque:

 cz.jirutka.rsql rsql-parser 2.1.0 

Et définissez également l'entité principale avec laquelle nous allons travailler tout au long des exemples - Utilisateur :

@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private int age; }

3. Analyser la demande

La façon dont les expressions RSQL sont représentées en interne se présente sous la forme de nœuds et le modèle de visiteur est utilisé pour analyser l'entrée.

Dans cet esprit, nous allons implémenter l' interface RSQLVisitor et créer notre propre implémentation de visiteur - CustomRsqlVisitor :

public class CustomRsqlVisitor implements RSQLVisitor
     
       { private GenericRsqlSpecBuilder builder; public CustomRsqlVisitor() { builder = new GenericRsqlSpecBuilder(); } @Override public Specification visit(AndNode node, Void param) { return builder.createSpecification(node); } @Override public Specification visit(OrNode node, Void param) { return builder.createSpecification(node); } @Override public Specification visit(ComparisonNode node, Void params) { return builder.createSecification(node); } }
     

Nous devons maintenant gérer la persistance et construire notre requête à partir de chacun de ces nœuds.

Nous allons utiliser les spécifications Spring Data JPA que nous avons utilisées auparavant - et nous allons implémenter un générateur de spécifications pour créer des spécifications à partir de chacun de ces nœuds que nous visitons :

public class GenericRsqlSpecBuilder { public Specification createSpecification(Node node) { if (node instanceof LogicalNode) { return createSpecification((LogicalNode) node); } if (node instanceof ComparisonNode) { return createSpecification((ComparisonNode) node); } return null; } public Specification createSpecification(LogicalNode logicalNode) { List specs = logicalNode.getChildren() .stream() .map(node -> createSpecification(node)) .filter(Objects::nonNull) .collect(Collectors.toList()); Specification result = specs.get(0); if (logicalNode.getOperator() == LogicalOperator.AND) { for (int i = 1; i < specs.size(); i++) { result = Specification.where(result).and(specs.get(i)); } } else if (logicalNode.getOperator() == LogicalOperator.OR) { for (int i = 1; i < specs.size(); i++) { result = Specification.where(result).or(specs.get(i)); } } return result; } public Specification createSpecification(ComparisonNode comparisonNode) { Specification result = Specification.where( new GenericRsqlSpecification( comparisonNode.getSelector(), comparisonNode.getOperator(), comparisonNode.getArguments() ) ); return result; } }

Notez comment:

  • LogicalNode est un nœud AND / OR et a plusieurs enfants
  • ComparisonNode n'a pas d'enfants et contient le sélecteur, l'opérateur et les arguments

Par exemple, pour une requête « nom == john » - nous avons:

  1. Sélecteur : "nom"
  2. Opérateur : "=="
  3. Arguments : [john]

4. Créer une spécification personnalisée

Lors de la construction de la requête, nous avons utilisé une spécification:

public class GenericRsqlSpecification implements Specification { private String property; private ComparisonOperator operator; private List arguments; @Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { List args = castArguments(root); Object argument = args.get(0); switch (RsqlSearchOperation.getSimpleOperator(operator)) { case EQUAL: { if (argument instanceof String) { return builder.like(root.get(property), argument.toString().replace('*', '%')); } else if (argument == null) { return builder.isNull(root.get(property)); } else { return builder.equal(root.get(property), argument); } } case NOT_EQUAL: { if (argument instanceof String) { return builder.notLike(root. get(property), argument.toString().replace('*', '%')); } else if (argument == null) { return builder.isNotNull(root.get(property)); } else { return builder.notEqual(root.get(property), argument); } } case GREATER_THAN: { return builder.greaterThan(root. get(property), argument.toString()); } case GREATER_THAN_OR_EQUAL: { return builder.greaterThanOrEqualTo(root. get(property), argument.toString()); } case LESS_THAN: { return builder.lessThan(root. get(property), argument.toString()); } case LESS_THAN_OR_EQUAL: { return builder.lessThanOrEqualTo(root. get(property), argument.toString()); } case IN: return root.get(property).in(args); case NOT_IN: return builder.not(root.get(property).in(args)); } return null; } private List castArguments(final Root root) { Class type = root.get(property).getJavaType(); List args = arguments.stream().map(arg -> { if (type.equals(Integer.class)) { return Integer.parseInt(arg); } else if (type.equals(Long.class)) { return Long.parseLong(arg); } else { return arg; } }).collect(Collectors.toList()); return args; } // standard constructor, getter, setter }

Notez que la spécification utilise des génériques et n'est liée à aucune entité spécifique (telle que l'utilisateur).

Ensuite - voici notre énumération « RsqlSearchOperation » qui contient les opérateurs rsql-parser par défaut:

public enum RsqlSearchOperation { EQUAL(RSQLOperators.EQUAL), NOT_EQUAL(RSQLOperators.NOT_EQUAL), GREATER_THAN(RSQLOperators.GREATER_THAN), GREATER_THAN_OR_EQUAL(RSQLOperators.GREATER_THAN_OR_EQUAL), LESS_THAN(RSQLOperators.LESS_THAN), LESS_THAN_OR_EQUAL(RSQLOperators.LESS_THAN_OR_EQUAL), IN(RSQLOperators.IN), NOT_IN(RSQLOperators.NOT_IN); private ComparisonOperator operator; private RsqlSearchOperation(ComparisonOperator operator) { this.operator = operator; } public static RsqlSearchOperation getSimpleOperator(ComparisonOperator operator) { for (RsqlSearchOperation operation : values()) { if (operation.getOperator() == operator) { return operation; } } return null; } }

5. Tester les requêtes de recherche

Commençons maintenant à tester nos nouvelles opérations flexibles à travers des scénarios réels:

Tout d'abord - initialisons les données:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceConfig.class }) @Transactional @TransactionConfiguration public class RsqlTest { @Autowired private UserRepository repository; private User userJohn; private User userTom; @Before public void init() { userJohn = new User(); userJohn.setFirstName("john"); userJohn.setLastName("doe"); userJohn.setEmail("[email protected]"); userJohn.setAge(22); repository.save(userJohn); userTom = new User(); userTom.setFirstName("tom"); userTom.setLastName("doe"); userTom.setEmail("[email protected]"); userTom.setAge(26); repository.save(userTom); } }

Testons maintenant les différentes opérations:

5.1. Tester l'égalité

Dans l'exemple suivant, nous rechercherons les utilisateurs par leur prénom et leur nom :

@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

5.2. Test de négation

Ensuite, recherchons les utilisateurs qui par leur prénom et non par «john»:

@Test public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName!=john"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }

5.3. Test supérieur à

Ensuite, nous rechercherons les utilisateurs âgés de plus de « 25 ans »:

@Test public void givenMinAge_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("age>25"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }

5.4. Tester comme

Ensuite, nous rechercherons les utilisateurs dont le prénom commence par « jo »:

@Test public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName==jo*"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

5.5. Test IN

Ensuite - nous rechercherons les utilisateurs dont le prénom est « john » ou « jack »:

@Test public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

6. UserController

Enfin - lions tout cela avec le contrôleur:

@RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List findAllByRsql(@RequestParam(value = "search") String search) { Node rootNode = new RSQLParser().parse(search); Specification spec = rootNode.accept(new CustomRsqlVisitor()); return dao.findAll(spec); }

Voici un exemple d'URL:

//localhost:8080/users?search=firstName==jo*;age<25

Et la réponse:

[{ "id":1, "firstName":"john", "lastName":"doe", "email":"[email protected]", "age":24 }]

7. Conclusion

Ce didacticiel a illustré comment créer un langage de requête / recherche pour une API REST sans avoir à réinventer la syntaxe et à utiliser à la place FIQL / RSQL.

L' implémentation complète de cet article se trouve dans le projet GitHub - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.

Suivant » Langage de requête REST avec support Web Querydsl « Précédent Langage de requête REST - Implémentation OU opération REST bas

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> DÉCOUVREZ LE COURS Persistence bottom

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS