Langage de requête REST avec RSQL
1. Vue d'ensemble
Dans ce cinquième article dethe series, nous allons illustrer la création du langage de requête de l'API REST à l'aide dea cool library – rsql-parser.
RSQL est un super-ensemble du langage de requête d'élément de fil (FIQL) - une syntaxe de filtre claire et simple pour les flux; il s'intègre donc tout naturellement dans une API REST. **
2. Les préparatifs
Tout d'abord, ajoutons une dépendance maven à la bibliothèque:
cz.jirutka.rsql
rsql-parser
2.0.0
Et aussidefine the main entity avec lesquels nous allons travailler tout au long des exemples -User:
@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 manière 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 lesRSQLVisitor interface et créer notre propre implémentation visiteur -CustomRsqlVisitor:
public class CustomRsqlVisitor implements RSQLVisitor, Void> {
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 JPAwe used before - et nous allons implémenter un générateurSpecification dansconstruct Specifications out of each of these nodes we visit:
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 unAND/ * OU *Node et a plusieurs enfants
-
ComparisonNode n'a pas d'enfants et contient lesSelector, Operator and the Arguments
Par exemple, pour une requête «name==john» - nous avons:
-
Selector: "nom"
-
Operator: «==»
-
Arguments: [john]
4. Créer desSpecification personnalisés
Lors de la construction de la requête, nous avons utilisé unSpecification:
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
Remarquez comment la spécification utilise des génériques et n'est liée à aucune entité spécifique (telle que l'utilisateur).
Ensuite, voici notreenum “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);
}
}
Maintenant, testons les différentes opérations:
5.1. Tester l'égalité
Dans l'exemple suivant, nous rechercherons les utilisateurs en fonction de leursfirst etlast name:
@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 leurfirst namene sont pas "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 avecage supérieur à «25»:
@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 lefirst name 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 lefirst name 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 au 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:
http://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 tutoriel explique comment construire un langage de requête / recherche pour une API REST sans avoir à réinventer la syntaxe et à utiliser à la place FIQL / RSQL.
Lesfull implementation de cet article se trouvent dansthe GitHub project - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.