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.