Langage de requête REST avec RSQL

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:

  1. Selector: "nom"

  2. Operator: «==»

  3. 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 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
}


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.