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

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

1. Vue d'ensemble

Dans ce didacticiel, nous cherchons à créer un langage de requête pour unREST API using Spring Data JPA and Querydsl.

Dans les deux premiers articles dethis series, nous avons créé la même fonctionnalité de recherche / filtrage à l'aide des critères JPA et des spécifications JPA Spring Data.

Donc -why a query language? Parce que - pour toute API assez complexe - rechercher / filtrer vos ressources par des champs très simples n'est tout simplement pas suffisant. A query language is more flexible, et vous permet de filtrer exactement les ressources dont vous avez besoin.

2. Configuration de Querydsl

Tout d'abord, voyons comment configurer notre projet pour utiliser Querydsl.

Nous devons ajouter les dépendances suivantes àpom.xml:


    com.querydsl
    querydsl-apt
    4.1.4
    

    com.querydsl
    querydsl-jpa
    4.1.4

Nous devons également configurer APT - Outil de traitement des annotations - plug-in comme suit:


    com.mysema.maven
    apt-maven-plugin
    1.1.3
    
        
            
                process
            
            
                target/generated-sources/java
                com.mysema.query.apt.jpa.JPAAnnotationProcessor
            
        
    

3. L'entitéMyUser

Ensuite, examinons l'entité "MyUser" que nous allons utiliser dans notre API de recherche:

@Entity
public class MyUser {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    private int age;
}

4. Predicate personnalisé avecPathBuilder

Maintenant, créons unPredicate personnalisé basé sur des contraintes arbitraires.

Nous utilisons iciPathBuilder au lieu des Q-types générés automatiquement, car nous devons créer des chemins de manière dynamique pour une utilisation plus abstraite:

public class MyUserPredicate {

    private SearchCriteria criteria;

    public BooleanExpression getPredicate() {
        PathBuilder entityPath = new PathBuilder<>(MyUser.class, "user");

        if (isNumeric(criteria.getValue().toString())) {
            NumberPath path = entityPath.getNumber(criteria.getKey(), Integer.class);
            int value = Integer.parseInt(criteria.getValue().toString());
            switch (criteria.getOperation()) {
                case ":":
                    return path.eq(value);
                case ">":
                    return path.goe(value);
                case "<":
                    return path.loe(value);
            }
        }
        else {
            StringPath path = entityPath.getString(criteria.getKey());
            if (criteria.getOperation().equalsIgnoreCase(":")) {
                return path.containsIgnoreCase(criteria.getValue().toString());
            }
        }
        return null;
    }
}

Notez comment l'implémentation du prédicat estgenerically dealing with multiple types of operations. En effet, le langage de requête est par définition un langage ouvert dans lequel vous pouvez potentiellement filtrer par n’importe quel champ, à l’aide de toute opération prise en charge.

Pour représenter ce type de critères de filtrage ouverts, nous utilisons une implémentation simple mais assez flexible -SearchCriteria:

public class SearchCriteria {
    private String key;
    private String operation;
    private Object value;
}

LeSearchCriteria contient les détails dont nous avons besoin pour représenter une contrainte:

  • key: le nom du champ - par exemple:firstName,age,… etc

  • operation: l'opération - par exemple: Egalité, inférieur à,… etc

  • value: la valeur du champ - par exemple: john, 25,… etc

5. MyUserRepository

Maintenant, jetons un œil à nosMyUserRepository.

Nous avons besoin de nosMyUserRepository pour étendreQuerydslPredicateExecutor afin de pouvoir utiliserPredicates plus tard pour filtrer les résultats de la recherche:

public interface MyUserRepository extends JpaRepository,
  QuerydslPredicateExecutor, QuerydslBinderCustomizer {
    @Override
    default public void customize(
      QuerydslBindings bindings, QMyUser root) {
        bindings.bind(String.class)
          .first((SingleValueBinding) StringExpression::containsIgnoreCase);
        bindings.excluding(root.email);
      }
}

6. CombinerPredicates

Ensuite, examinons la possibilité de combiner des prédicats pour utiliser plusieurs contraintes dans le filtrage des résultats.

Dans l'exemple suivant - nous travaillons avec un générateur -MyUserPredicatesBuilder - pour combinerPredicates:

public class MyUserPredicatesBuilder {
    private List params;

    public MyUserPredicatesBuilder() {
        params = new ArrayList<>();
    }

    public MyUserPredicatesBuilder with(
      String key, String operation, Object value) {

        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

    public BooleanExpression build() {
        if (params.size() == 0) {
            return null;
        }

        List predicates = params.stream().map(param -> {
            MyUserPredicate predicate = new MyUserPredicate(param);
            return predicate.getPredicate();
        }).filter(Objects::nonNull).collect(Collectors.toList());

        BooleanExpression result = Expressions.asBoolean(true).isTrue();
        for (BooleanExpression predicate : predicates) {
            result = result.and(predicate);
        }
        return result;
    }
}

7. Tester les requêtes de recherche

Ensuite, testons notre API de recherche.

Nous allons commencer par initialiser la base de données avec quelques utilisateurs - pour que ces derniers soient prêts et disponibles pour les tests:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@Rollback
public class JPAQuerydslIntegrationTest {

    @Autowired
    private MyUserRepository repo;

    private MyUser userJohn;
    private MyUser userTom;

    @Before
    public void init() {
        userJohn = new MyUser();
        userJohn.setFirstName("John");
        userJohn.setLastName("Doe");
        userJohn.setEmail("[email protected]");
        userJohn.setAge(22);
        repo.save(userJohn);

        userTom = new MyUser();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("[email protected]");
        userTom.setAge(26);
        repo.save(userTom);
    }
}

Voyons ensuite comment trouver des utilisateurs avecgiven last name:

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, containsInAnyOrder(userJohn, userTom));
}

Voyons maintenant comment trouver un utilisateur avec desboth first and last name donnés:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "John").with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

Voyons ensuite comment trouver un utilisateur avec desboth last name and minimum age donnés

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("lastName", ":", "Doe").with("age", ">", "25");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userTom));
    assertThat(results, not(contains(userJohn)));
}

Voyons maintenant comment rechercherMyUser quedoesn’t actually exist:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "Adam").with("lastName", ":", "Fox");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, emptyIterable());
}

Enfin, voyons comment trouver unMyUsergiven only part of the first name - comme dans l'exemple suivant:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

8. UserController

Enfin, rassemblons tout et créons l'API REST.

Nous définissons unUserController qui définit une méthode simplefindAll() avec un paramètre «search» à transmettre dans la chaîne de requête:

@Controller
public class UserController {

    @Autowired
    private MyUserRepository myUserRepository;

    @RequestMapping(method = RequestMethod.GET, value = "/myusers")
    @ResponseBody
    public Iterable search(@RequestParam(value = "search") String search) {
        MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder();

        if (search != null) {
            Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),");
            Matcher matcher = pattern.matcher(search + ",");
            while (matcher.find()) {
                builder.with(matcher.group(1), matcher.group(2), matcher.group(3));
            }
        }
        BooleanExpression exp = builder.build();
        return myUserRepository.findAll(exp);
    }
}

Voici un exemple d'URL de test rapide:

http://localhost:8080/myusers?search=lastName:doe,age>25

Et la réponse:

[{
    "id":2,
    "firstName":"tom",
    "lastName":"doe",
    "email":"[email protected]",
    "age":26
}]

9. Conclusion

Ce troisième article portait surthe first steps of building a query language for a REST API, faisant bon usage de la bibliothèque Querydsl.

La mise en œuvre est certes précoce, mais elle peut facilement être modifiée pour prendre en charge des opérations supplémentaires.

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.