Langage de requête REST avec spécifications Spring Data JPA

Spécifications du langage de requête REST avec Spring Data JPA

1. Vue d'ensemble

Dans ce didacticiel, nous allons créer unSearch/Filter REST API à l'aide de Spring Data JPA et des spécifications.

Nous avons commencé à examiner un langage de requête dans lesfirst article dethis series - avec une solution basée sur des critères JPA.

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

Tout d'abord, commençons par une simple entitéUser pour notre API de recherche:

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

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

    private int age;

    // standard getters and setters
}

3. Filtrer avecSpecification

Maintenant, passons directement à la partie la plus intéressante du problème: les requêtes avec des Spring Data JPASpecificationspersonnalisés.

Nous allons créer unUserSpecification qui implémente l'interfaceSpecification et nous allons verspass in our own constraint to construct the actual query:

public class UserSpecification implements Specification {

    private SearchCriteria criteria;

    @Override
    public Predicate toPredicate
      (Root root, CriteriaQuery query, CriteriaBuilder builder) {

        if (criteria.getOperation().equalsIgnoreCase(">")) {
            return builder.greaterThanOrEqualTo(
              root. get(criteria.getKey()), criteria.getValue().toString());
        }
        else if (criteria.getOperation().equalsIgnoreCase("<")) {
            return builder.lessThanOrEqualTo(
              root. get(criteria.getKey()), criteria.getValue().toString());
        }
        else if (criteria.getOperation().equalsIgnoreCase(":")) {
            if (root.get(criteria.getKey()).getJavaType() == String.class) {
                return builder.like(
                  root.get(criteria.getKey()), "%" + criteria.getValue() + "%");
            } else {
                return builder.equal(root.get(criteria.getKey()), criteria.getValue());
            }
        }
        return null;
    }
}

Comme nous pouvons le voir -we create a Specification based on some simple constrains que nous représentons dans la classe «SearchCriteria» suivante:

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

L'implémentation deSearchCriteria contient une représentation de base d'une contrainte - et c'est sur la base de cette contrainte que nous allons construire la requête:

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

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

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

Bien entendu, la mise en œuvre est simpliste et peut être améliorée. c'est cependant une base solide pour les opérations puissantes et flexibles dont nous avons besoin.

4. LesUserRepository

Ensuite, jetons un œil auxUserRepository; nous étendons simplement lesJpaSpecificationExecutor pour obtenir les nouvelles API de spécification:

public interface UserRepository
  extends JpaRepository, JpaSpecificationExecutor {}

5. Tester les requêtes de recherche

Maintenant, testons la nouvelle API de recherche.

Commençons par créer quelques utilisateurs pour qu'ils soient prêts lors de l'exécution des tests:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceJPAConfig.class })
@Transactional
@TransactionConfiguration
public class JPASpecificationsTest {

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

Voyons ensuite comment trouver des utilisateurs avecgiven last name:

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec =
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));

    List results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, isIn(results));
}

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

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 =
      new UserSpecification(new SearchCriteria("firstName", ":", "john"));
    UserSpecification spec2 =
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));

    List results = repository.findAll(Specification.where(spec1).and(spec2));

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

Remarque: Nous avons utilisé «where» et «and» àcombine Specifications.

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

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 =
      new UserSpecification(new SearchCriteria("age", ">", "25"));
    UserSpecification spec2 =
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));

    List results =
      repository.findAll(Specification.where(spec1).and(spec2));

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

Voyons maintenant comment rechercherUser quedoesn’t actually exist:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 =
      new UserSpecification(new SearchCriteria("firstName", ":", "Adam"));
    UserSpecification spec2 =
      new UserSpecification(new SearchCriteria("lastName", ":", "Fox"));

    List results =
      repository.findAll(Specification.where(spec1).and(spec2));

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

Enfin, voyons comment trouver unUsergiven only part of the first name:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec =
      new UserSpecification(new SearchCriteria("firstName", ":", "jo"));

    List results = repository.findAll(spec);

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

6. CombinerSpecifications

Ensuite, jetons un œil à la combinaison de nosSpecifications personnalisés pour utiliser plusieurs contraintes et filtrer en fonction de plusieurs critères.

Nous allons implémenter un générateur -UserSpecificationsBuilder - pour combiner facilement et couramment lesSpecifications:

public class UserSpecificationsBuilder {

    private final List params;

    public UserSpecificationsBuilder() {
        params = new ArrayList();
    }

    public UserSpecificationsBuilder with(String key, String operation, Object value) {
        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

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

        List specs = params.stream()
          .map(UserSpecification::new)
          .collect(Collectors.toList());

        Specification result = specs.get(0);

        for (int i = 1; i < params.size(); i++) {
            result = params.get(i)
              .isOrPredicate()
                ? Specification.where(result)
                  .or(specs.get(i))
                : Specification.where(result)
                  .and(specs.get(i));
        }
        return result;
    }
}

7. UserController

Enfin - utilisons cette nouvelle fonctionnalité de recherche / filtrage de persistance etset up the REST API - en créant unUserController avec une simple opérationsearch:

@Controller
public class UserController {

    @Autowired
    private UserRepository repo;

    @RequestMapping(method = RequestMethod.GET, value = "/users")
    @ResponseBody
    public List search(@RequestParam(value = "search") String search) {
        UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
        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));
        }

        Specification spec = builder.build();
        return repo.findAll(spec);
    }
}

Notez que pour prendre en charge d'autres systèmes non anglais, l'objetPattern peut être modifié comme:

Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),", Pattern.UNICODE_CHARACTER_CLASS);

Voici un exemple d'URL de test pour tester l'API:

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

Et la réponse:

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

Since the searches are split by a “,” in our Pattern example, the search terms can’t contain this character. Le motif ne correspond pas non plus aux espaces.

Si nous souhaitons rechercher des valeurs contenant des virgules, nous pouvons envisager d'utiliser un séparateur différent, tel que «;».

Une autre option serait de changer le motif pour rechercher des valeurs entre guillemets, puis de les supprimer du terme recherché:

Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\"([^\"]+)\")");

8. Conclusion

Ce tutoriel couvrait une implémentation simple pouvant constituer la base d'un puissant langage de requête REST. Nous avons fait bon usage des spécifications de Spring Data pour nous assurer de garder l'API à l'écart du domaine et deshave the option to handle many other types of operations.

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.