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

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 (article actuel) • Langage de requête REST - Opérations de recherche avancées

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

• Langage de requête REST avec RSQL

• Langage de requête REST avec support Web Querydsl

1. Vue d'ensemble

Dans ce didacticiel, nous cherchons à créer un langage de requête pour une API REST à l'aide de Spring Data JPA et Querydsl .

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

Alors - pourquoi un langage de requête? Parce que - pour toute API assez complexe - rechercher / filtrer vos ressources par des champs très simples n'est tout simplement pas suffisant. Un langage de requête est plus 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.2.2   com.querydsl querydsl-jpa 4.2.2 

Nous devons également configurer le plugin APT - Annotation processing tool - plugin comme suit:

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

Cela générera les types Q pour nos entités.

3. L' entité MyUser

Ensuite, jetons un coup d'œil à 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. Prédicat personnalisé avec PathBuilder

Maintenant, créons un prédicat personnalisé basé sur des contraintes arbitraires.

Nous utilisons PathBuilder ici au lieu des types Q 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 traite de manière générique plusieurs types d'opérations . 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, en utilisant n'importe quelle 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; }

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

  • clé : le nom du champ - par exemple: prénom , âge ,… etc
  • opération : l'opération - par exemple: égalité, inférieur à,… etc
  • value : la valeur du champ - par exemple: john, 25,… etc

5. MyUserRepository

Maintenant, jetons un coup d'œil à notre MyUserRepository .

Nous avons besoin de notre MyUserRepository pour étendre QuerydslPredicateExecutor afin que nous puissions utiliser les prédicats 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); } }

Notez que nous utilisons ici le type Q généré pour l' entité MyUser , qui s'appellera QMyUser.

6. Combiner les prédicats

Ensuite, jetons un œil à la combinaison de 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 combiner des prédicats :

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. Testez les requêtes de recherche

Ensuite, testons notre API de recherche.

Nous commencerons par initialiser la base de données avec quelques utilisateurs - pour les avoir 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); } }

Ensuite, voyons comment trouver des utilisateurs avec un nom de famille donné :

@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 un prénom et un nom 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))); }

Next, let’s see how to find user with given both last name and minimum age

@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))); }

Now, let’s see how to search for MyUser that doesn’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()); }

Finally – let’s see how to find a MyUser given only part of the first name – as in the following example:

@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

Finally, let's put everything together and build the REST API.

We're defining a UserController that defines a simple method findAll() with a “search“ parameter to pass in the query string:

@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); } }

Here is a quick test URL example:

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

And the response:

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

9. Conclusion

Ce troisième article a couvert les premières étapes de création d'un langage de requête pour une API REST , en utilisant à bon escient la bibliothèque Querydsl.

La mise en œuvre est bien sûr précoce, mais elle peut facilement évoluer pour prendre en charge des opérations supplémentaires.

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 - Opérations de recherche avancées « Précédent Langage de requête REST avec spécifications Spring Data JPA