Langage de requête REST - Opérations de recherche avancée

Langage de requête REST - Opérations de recherche avancée

1. Vue d'ensemble

Dans cet article, nous allons étendre le langage de requête REST que nous avons développé enthe previous parts dethe series àinclude more search operations.

Nous prenons maintenant en charge les opérations suivantes: égalité, négation, supérieure à, inférieure à, commence par, se termine par, contient et aime.

Notez que nous avons exploré trois implémentations - JPA Criteria, Spring Data JPA Specifications et Query DSL; nous allons de l'avant avec les spécifications dans cet article, car il s'agit d'une manière claire et flexible de représenter nos opérations.

2. LesSearchOperationenum

Tout d'abord, commençons par définir une meilleure représentation de nos différentes opérations de recherche prises en charge - via une énumération:

public enum SearchOperation {
    EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE, STARTS_WITH, ENDS_WITH, CONTAINS;

    public static final String[] SIMPLE_OPERATION_SET = { ":", "!", ">", "<", "~" };

    public static SearchOperation getSimpleOperation(char input) {
        switch (input) {
        case ':':
            return EQUALITY;
        case '!':
            return NEGATION;
        case '>':
            return GREATER_THAN;
        case '<':
            return LESS_THAN;
        case '~':
            return LIKE;
        default:
            return null;
        }
    }
}

Nous avons deux ensembles d'opérations:

1. Simple - peut être représenté par un caractère:

  • Égalité: représentée par deux points (:)

  • Négation: représentée par un point d'exclamation (!)

  • Supérieur à: représenté par (>)

  • Inférieur à: représenté par (<)

  • Comme: représenté par tilde (~)

2. Complex - nécessite plus d'un caractère pour être représenté:

  • Commence par: représenté par (=prefix*)

  • Se termine par: représenté par (=*suffix)

  • Contient: représenté par (=*substring*)

Nous devons également modifier notre classeSearchCriteria pour utiliser les nouveauxSearchOperation:

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

3. ModifierUserSpecification

Maintenant, incluons les opérations nouvellement prises en charge dans notre implémentationUserSpecification:

public class UserSpecification implements Specification {

    private SearchCriteria criteria;

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

        switch (criteria.getOperation()) {
        case EQUALITY:
            return builder.equal(root.get(criteria.getKey()), criteria.getValue());
        case NEGATION:
            return builder.notEqual(root.get(criteria.getKey()), criteria.getValue());
        case GREATER_THAN:
            return builder.greaterThan(root. get(
              criteria.getKey()), criteria.getValue().toString());
        case LESS_THAN:
            return builder.lessThan(root. get(
              criteria.getKey()), criteria.getValue().toString());
        case LIKE:
            return builder.like(root. get(
              criteria.getKey()), criteria.getValue().toString());
        case STARTS_WITH:
            return builder.like(root. get(criteria.getKey()), criteria.getValue() + "%");
        case ENDS_WITH:
            return builder.like(root. get(criteria.getKey()), "%" + criteria.getValue());
        case CONTAINS:
            return builder.like(root. get(
              criteria.getKey()), "%" + criteria.getValue() + "%");
        default:
            return null;
        }
    }
}

4. Tests de persistance

Ensuite - nous testons nos nouvelles opérations de recherche - au niveau de la persistance:

4.1. Tester l'égalité

Dans l'exemple suivant, nous rechercherons un utilisateurby their first and last name:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.EQUALITY, "john"));
    UserSpecification spec1 = new UserSpecification(
      new SearchCriteria("lastName", SearchOperation.EQUALITY, "doe"));
    List results = repository.findAll(Specification.where(spec).and(spec1));

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

4.2. Test de négation

Ensuite, recherchons les utilisateurs qui, partheir first name not “john”:

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.NEGATION, "john"));
    List results = repository.findAll(Specification.where(spec));

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

4.3. Test supérieur à

Ensuite - nous rechercherons les utilisateurs avecage greater than “25”:

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("age", SearchOperation.GREATER_THAN, "25"));
    List results = repository.findAll(Specification.where(spec));

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

4.4. Le test commence par

Suivant - utilisateurs avectheir first name starting with “jo”:

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.STARTS_WITH, "jo"));
    List results = repository.findAll(spec);

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

4.5. Le test se termine par

Ensuite, nous rechercherons les utilisateurs avectheir first name ending with “n”:

@Test
public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.ENDS_WITH, "n"));
    List results = repository.findAll(spec);

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

4.6. Le test contient

Maintenant, nous allons rechercher les utilisateurs avectheir first name containing “oh”:

@Test
public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.CONTAINS, "oh"));
    List results = repository.findAll(spec);

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

4.7. Gamme de test

Enfin, nous rechercherons les utilisateurs avecages between “20” and “25”:

@Test
public void givenAgeRange_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("age", SearchOperation.GREATER_THAN, "20"));
    UserSpecification spec1 = new UserSpecification(
      new SearchCriteria("age", SearchOperation.LESS_THAN, "25"));
    List results = repository.findAll(Specification.where(spec).and(spec1));

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

5. LesUserSpecificationBuilder

Maintenant que la persistance est terminée et testée, portons notre attention sur la couche Web.

Nous allons nous baser sur l'implémentation deUserSpecificationBuilder de l'article précédent versincorporate the new new search operations:

public class UserSpecificationsBuilder {

    private List params;

    public UserSpecificationsBuilder with(
      String key, String operation, Object value, String prefix, String suffix) {

        SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
        if (op != null) {
            if (op == SearchOperation.EQUALITY) {
                boolean startWithAsterisk = prefix.contains("*");
                boolean endWithAsterisk = suffix.contains("*");

                if (startWithAsterisk && endWithAsterisk) {
                    op = SearchOperation.CONTAINS;
                } else if (startWithAsterisk) {
                    op = SearchOperation.ENDS_WITH;
                } else if (endWithAsterisk) {
                    op = SearchOperation.STARTS_WITH;
                }
            }
            params.add(new SearchCriteria(key, op, value));
        }
        return this;
    }

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

        Specification result = new UserSpecification(params.get(0));

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

        return result;
    }
}

6. LesUserController

Ensuite, nous devons modifier nosUserController enparse the new operations correctement:

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List findAllBySpecification(@RequestParam(value = "search") String search) {
    UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
    String operationSetExper = Joiner.on("|").join(SearchOperation.SIMPLE_OPERATION_SET);
    Pattern pattern = Pattern.compile(
      "(\\w+?)(" + operationSetExper + ")(\p{Punct}?)(\\w+?)(\p{Punct}?),");
    Matcher matcher = pattern.matcher(search + ",");
    while (matcher.find()) {
        builder.with(
          matcher.group(1),
          matcher.group(2),
          matcher.group(4),
          matcher.group(3),
          matcher.group(5));
    }

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

Nous pouvons maintenant utiliser l'API et obtenir les bons résultats avec n'importe quelle combinaison de critères. Par exemple, voici à quoi ressemblerait une opération complexe en utilisant l'API avec le langage de requête:

http://localhost:8080/users?search=firstName:jo*,age<25

Et la réponse:

[{
    "id":1,
    "firstName":"john",
    "lastName":"doe",
    "email":"[email protected]",
    "age":24
}]

7. Tests pour l'API de recherche

Enfin, assurons-nous que notre API fonctionne bien en écrivant une suite de tests d'API.

Nous allons commencer par la configuration simple du test et l'initialisation des données:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  classes = { ConfigTest.class, PersistenceConfig.class },
  loader = AnnotationConfigContextLoader.class)
@ActiveProfiles("test")
public class JPASpecificationLiveTest {

    @Autowired
    private UserRepository repository;

    private User userJohn;
    private User userTom;

    private final String URL_PREFIX = "http://localhost:8080/users?search=";

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

    private RequestSpecification givenAuth() {
        return RestAssured.given().auth()
                                  .preemptive()
                                  .basic("username", "password");
    }
}

7.1. Tester l'égalité

Commençons par rechercher un utilisateur avecthe first name “john” and last name “doe:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:john,lastName:doe");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.2. Test de négation

Maintenant, nous rechercherons des utilisateurs lorsquetheir first name isn’t “john”:

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName!john");
    String result = response.body().asString();

    assertTrue(result.contains(userTom.getEmail()));
    assertFalse(result.contains(userJohn.getEmail()));
}

7.3. Test supérieur à

Ensuite - nous chercherons les utilisateurs avecage greater than “25”:

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "age>25");
    String result = response.body().asString();

    assertTrue(result.contains(userTom.getEmail()));
    assertFalse(result.contains(userJohn.getEmail()));
}

7.4. Le test commence par

Suivant - utilisateurs avectheir first name starting with “jo”:

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:jo*");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.5. Le test se termine par

Maintenant - utilisateurs avectheir first name ending with “n”:

@Test
public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:*n");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.6. Le test contient

Ensuite, nous rechercherons les utilisateurs avectheir first name containing “oh”:

@Test
public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:*oh*");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.7. Gamme de test

Enfin, nous rechercherons les utilisateurs avecages between “20” and “25”:

@Test
public void givenAgeRange_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "age>20,age<25");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

8. Conclusion

Dans cet article, nous avons transmis le langage de requête de notre API de recherche REST àa mature, tested, production-grade implementation. Nous prenons désormais en charge une grande variété d’opérations et de contraintes, ce qui devrait faciliter le découpage élégant de n’importe quel ensemble de données et accéder aux ressources exactes que nous recherchons.

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.