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

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

1. Vue d'ensemble

Dans ce premier article dethis new series, nous allons explorera simple query language for a REST API. Nous ferons bon usage de Spring pour l'API REST et des critères JPA 2 pour les aspects de persistance.

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. Un langage de requête est plus flexible et vous permet de filtrer exactement les ressources dont vous avez besoin.

2. EntitéUser

Tout d'abord, présentons l'entité simple que nous allons utiliser pour notre API de filtrage / recherche - unUser basique:

@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 avecCriteriaBuilder

Passons maintenant à l'essentiel du problème: la requête dans la couche de persistance.

Construire une requête L'abstraction est une question d'équilibre. Nous avons besoin d’une grande flexibilité d’une part et de la gestion de la complexité de l’autre. Haut niveau, la fonctionnalité est simple -you pass in some constraints and you get back some results.

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 œil à la classeUserSearchQueryCriteriaConsumer:

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'APIsearchUser 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 deSearchCriteria contient nos paramètresQuery:

  • key: utilisé pour contenir le nom du champ - par exemple:firstName,age,… etc.

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

  • value: utilisé pour contenir la valeur du champ - par exemple: john, 25,… etc.

4. Tester les requêtes de recherche

Maintenant, testons notre mécanisme de recherche pour nous assurer qu'il tient la route.

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, obtenons unUser avec desfirstName etlastName spécifiques - 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 unList deUser avec les mêmeslastName:

@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 avecagegreater than or equal 25:

@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 les utilisateurs quidon’t actually exist:

@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 dont seulspartialfirstName sont fournis:

@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. LesUserController

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

Nous allons configurer un simpleUserController - avec unfindAll()using the “search” to pass in the entire search/filter expression:

@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 comment 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:

http://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 filtrage intelligent sur une API REST. Oui, il est encore difficile sur les bords et peut être amélioré (et sera amélioré dans le prochain article) - mais c'est un bon point de départ pour implémenter ce type de fonctionnalité de filtrage sur vos API.

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.