Langage de requête REST avec critères Spring et JPA

Haut REST

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 (article actuel) • Langage de requête REST avec Spring Data Spécifications 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

• Langage de requête REST avec RSQL

• Langage de requête REST avec support Web Querydsl

1. Vue d'ensemble

Dans ce premier article de cette nouvelle série, nous explorerons un langage de requête simple pour une API REST . Nous ferons bon usage de Spring pour l'API REST et des critères JPA 2 pour les aspects de persistance.

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. Entité utilisateur

Tout d'abord, mettons en avant l'entité simple que nous allons utiliser pour notre API de filtrage / recherche - un utilisateur de base :

@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. Filtrer à l'aide de CriteriaBuilder

Maintenant - entrons dans le cœur du problème - la requête dans la couche de persistance.

Construire une abstraction de requête est une question d'équilibre. Nous avons besoin d'une bonne flexibilité d'une part, et nous devons garder la complexité gérable d'autre part. De haut niveau, la fonctionnalité est simple - vous transmettez certaines contraintes et vous obtenez des résultats .

Voyons comment cela fonctionne:

@Repository public class UserDAO implements IUserDAO { @PersistenceContext private EntityManager entityManager; @Override public List searchUser(List params) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(User.class); Root r = query.from(User.class); Predicate predicate = builder.conjunction(); UserSearchQueryCriteriaConsumer searchConsumer = new UserSearchQueryCriteriaConsumer(predicate, builder, r); params.stream().forEach(searchConsumer); predicate = searchConsumer.getPredicate(); query.where(predicate); List result = entityManager.createQuery(query).getResultList(); return result; } @Override public void save(User entity) { entityManager.persist(entity); } }

Jetons un coup d'œil à la classe UserSearchQueryCriteriaConsumer :

public class UserSearchQueryCriteriaConsumer implements Consumer{ private Predicate predicate; private CriteriaBuilder builder; private Root r; @Override public void accept(SearchCriteria param) { if (param.getOperation().equalsIgnoreCase(">")) { predicate = builder.and(predicate, builder .greaterThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase("<")) { predicate = builder.and(predicate, builder.lessThanOrEqualTo( r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase(":")) { if (r.get(param.getKey()).getJavaType() == String.class) { predicate = builder.and(predicate, builder.like( r.get(param.getKey()), "%" + param.getValue() + "%")); } else { predicate = builder.and(predicate, builder.equal( r.get(param.getKey()), param.getValue())); } } } // standard constructor, getter, setter }

Comme vous pouvez le voir, l' API searchUser prend une liste de contraintes très simples, compose une requête basée sur ces contraintes, effectue la recherche et renvoie les résultats.

La classe de contrainte est également assez simple:

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

L' implémentation SearchCriteria contient nos paramètres de requête :

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

4. Testez les requêtes de recherche

Maintenant, testons notre mécanisme de recherche pour nous assurer qu'il retient l'eau.

Tout d'abord - initialisons notre base de données pour les tests en ajoutant deux utilisateurs - comme dans l'exemple suivant:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceConfig.class }) @Transactional @TransactionConfiguration public class JPACriteriaQueryTest { @Autowired private IUserDAO userApi; 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); userApi.save(userJohn); userTom = new User(); userTom.setFirstName("Tom"); userTom.setLastName("Doe"); userTom.setEmail("[email protected]"); userTom.setAge(26); userApi.save(userTom); } }

Maintenant, nous allons obtenir un utilisateur avec spécifique firstName et lastName - comme dans l'exemple suivant:

@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { List params = new ArrayList(); params.add(new SearchCriteria("firstName", ":", "John")); params.add(new SearchCriteria("lastName", ":", "Doe")); List results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

Ensuite, obtenons une liste d' utilisateurs avec le même nom :

@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { List params = new ArrayList(); params.add(new SearchCriteria("lastName", ":", "Doe")); List results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results)); }

Ensuite, obtenons les utilisateurs d' âge supérieur ou égal à 25 ans :

@Test public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { List params = new ArrayList(); params.add(new SearchCriteria("lastName", ":", "Doe")); params.add(new SearchCriteria("age", ">", "25")); List results = userApi.searchUser(params); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }

Ensuite, recherchons des utilisateurs qui n'existent pas réellement :

@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { List params = new ArrayList(); params.add(new SearchCriteria("firstName", ":", "Adam")); params.add(new SearchCriteria("lastName", ":", "Fox")); List results = userApi.searchUser(params); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results))); }

Enfin, recherchons les utilisateurs avec un prénom partiel :

@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { List params = new ArrayList(); params.add(new SearchCriteria("firstName", ":", "jo")); List results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

6. Le UserController

Enfin, connectons maintenant la prise en charge de la persistance de cette recherche flexible à notre API REST.

Nous allons mettre en place un UserController simple - avec un findAll () en utilisant la " recherche " pour transmettre l'expression de recherche / filtre entière :

@Controller public class UserController { @Autowired private IUserDao api; @RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List findAll(@RequestParam(value = "search", required = false) String search) { List params = new ArrayList(); if (search != null) { Pattern pattern = Pattern.compile("(\w+?)(:|)(\w+?),"); Matcher matcher = pattern.matcher(search + ","); while (matcher.find()) { params.add(new SearchCriteria(matcher.group(1), matcher.group(2), matcher.group(3))); } } return api.searchUser(params); } }

Notez que nous créons simplement nos objets de critères de recherche à partir de l'expression de recherche.

Nous sommes maintenant au point où nous pouvons commencer à jouer avec l'API et nous assurer que tout fonctionne correctement:

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

Et voici sa réponse:

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

7. Conclusion

Cette implémentation simple mais puissante permet un peu de filtrage intelligent sur une API REST. Oui - c'est encore difficile sur les bords et peut être amélioré (et sera amélioré dans le prochain article) - mais c'est un point de départ solide pour implémenter ce type de fonctionnalité de filtrage sur vos API.

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 Spring Data Spécifications JPA REST bas

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