Linguagem de Consulta REST - Operações de Pesquisa Avançada
1. Visão geral
Neste artigo, vamos estender a linguagem de consulta REST que desenvolvemos emthe previous parts dethe series parainclude more search operations.
Agora, oferecemos suporte às seguintes operações: Igualdade, Negação, Maior que, Menor que, Inicia, Termina com, Contém e Gosto.
Observe que exploramos três implementações - critérios JPA, especificações Spring Data JPA e DSL de consulta; estamos avançando com as especificações neste artigo porque é uma maneira limpa e flexível de representar nossas operações.
2. OSearchOperationenum
Primeiro - vamos começar definindo uma representação melhor de nossas várias operações de pesquisa com suporte - por meio de uma enumeração:
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;
}
}
}
Temos dois conjuntos de operações:
1. Simple - pode ser representado por um caractere:
-
Igualdade: representada por dois pontos (:)
-
Negação: representada por ponto de exclamação (!)
-
Maior que: representado por (>)
-
Menor que: representado por (<)
-
Like: representado por til (~)
2. Complex - precisa de mais de um caractere para ser representado:
-
Começa com: representado por (=prefix*)
-
Termina com: representado por (=*suffix)
-
Contém: representado por (=*substring*)
Também precisamos modificar nossa classeSearchCriteria para usar o novoSearchOperation:
public class SearchCriteria {
private String key;
private SearchOperation operation;
private Object value;
}
3. ModificarUserSpecification
Agora - vamos incluir as novas operações com suporte em nossa implementaçãoUserSpecification:
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. Testes de Persistência
A seguir - vamos testar nossas novas operações de pesquisa - no nível de persistência:
4.1. Igualdade de teste
No exemplo a seguir - pesquisaremos um usuárioby 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. Negação de Teste
A seguir, vamos procurar usuários que portheir 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. Teste maior que
A seguir - pesquisaremos usuários comage 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. O teste começa com
Próximo - usuários comtheir 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. Teste termina com
A seguir, pesquisaremos usuários comtheir 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. O teste contém
Agora, vamos procurar usuários comtheir 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. Faixa de Teste
Por fim, pesquisaremos usuários comages 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. OUserSpecificationBuilder
Agora que a persistência foi feita e testada, vamos voltar nossa atenção para a camada da web.
Vamos construir sobre a implementação deUserSpecificationBuilder do artigo anterior paraincorporate 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. OUserController
Em seguida - precisamos modificar nossoUserController paraparse the new operations corretamente:
@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);
}
Agora podemos acessar a API e obter os resultados certos com qualquer combinação de critérios. Por exemplo - veja como uma operação complexa seria usando API com a linguagem de consulta:
http://localhost:8080/users?search=firstName:jo*,age<25
E a resposta:
[{
"id":1,
"firstName":"john",
"lastName":"doe",
"email":"[email protected]",
"age":24
}]
7. Testes para a API de pesquisa
Finalmente - vamos garantir que nossa API funcione bem escrevendo um conjunto de testes de API.
Começaremos com a configuração simples do teste e a inicialização dos dados:
@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. Igualdade de teste
Primeiro - vamos procurar um usuário comthe 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. Negação de Teste
Agora - pesquisaremos usuários quandotheir 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. Teste maior que
A seguir - procuraremos usuários comage 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. O teste começa com
Próximo - usuários comtheir 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. Teste termina com
Agora - usuários comtheir 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. O teste contém
A seguir, pesquisaremos usuários comtheir 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. Faixa de Teste
Por fim, pesquisaremos usuários comages 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. Conclusão
Neste artigo, trouxemos a linguagem de consulta de nossa API de pesquisa REST paraa mature, tested, production-grade implementation. Agora oferecemos suporte a uma ampla variedade de operações e restrições, o que deve tornar muito fácil cortar qualquer conjunto de dados com elegância e obter os recursos exatos que procuramos.
Ofull implementation deste artigo pode ser encontrado emthe GitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.