Linguagem de Consulta REST com Spring Data JPA e Querydsl
1. Visão geral
Neste tutorial, estamos construindo uma linguagem de consulta para umREST API using Spring Data JPA and Querydsl.
Nos primeiros dois artigos dethis series, construímos a mesma funcionalidade de pesquisa / filtragem usando os critérios JPA e as especificações Spring Data JPA.
Então -why a query language? Porque - para qualquer API suficientemente complexa - pesquisar / filtrar seus recursos por campos muito simples simplesmente não é suficiente. A query language is more flexible, e permite filtrar exatamente os recursos de que você precisa.
2. Configuração Querydsl
Primeiro - vamos ver como configurar nosso projeto para usar Querydsl.
Precisamos adicionar as seguintes dependências apom.xml:
com.querydsl
querydsl-apt
4.1.4
com.querydsl
querydsl-jpa
4.1.4
Também precisamos configurar o plug-in APT - Annotation processing tool - da seguinte maneira:
com.mysema.maven
apt-maven-plugin
1.1.3
process
target/generated-sources/java
com.mysema.query.apt.jpa.JPAAnnotationProcessor
3. A EntidadeMyUser
A seguir - vamos dar uma olhada na entidade “MyUser” que usaremos em nossa API de pesquisa:
@Entity
public class MyUser {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String email;
private int age;
}
4. Predicate personalizado comPathBuilder
Agora - vamos criar umPredicate personalizado com base em algumas restrições arbitrárias.
Estamos usandoPathBuilder aqui em vez dos Q-types gerados automaticamente porque precisamos criar caminhos dinamicamente para um uso mais abstrato:
public class MyUserPredicate {
private SearchCriteria criteria;
public BooleanExpression getPredicate() {
PathBuilder entityPath = new PathBuilder<>(MyUser.class, "user");
if (isNumeric(criteria.getValue().toString())) {
NumberPath path = entityPath.getNumber(criteria.getKey(), Integer.class);
int value = Integer.parseInt(criteria.getValue().toString());
switch (criteria.getOperation()) {
case ":":
return path.eq(value);
case ">":
return path.goe(value);
case "<":
return path.loe(value);
}
}
else {
StringPath path = entityPath.getString(criteria.getKey());
if (criteria.getOperation().equalsIgnoreCase(":")) {
return path.containsIgnoreCase(criteria.getValue().toString());
}
}
return null;
}
}
Observe como a implementação do predicado égenerically dealing with multiple types of operations. Isso ocorre porque o idioma da consulta é, por definição, um idioma aberto, no qual você pode potencialmente filtrar por qualquer campo, usando qualquer operação suportada.
Para representar esse tipo de critério de filtragem aberto, estamos usando uma implementação simples, mas bastante flexível -SearchCriteria:
public class SearchCriteria {
private String key;
private String operation;
private Object value;
}
OSearchCriteria contém os detalhes de que precisamos para representar uma restrição:
-
key: o nome do campo - por exemplo:firstName,age, ... etc
-
operation: a operação - por exemplo: Igualdade, menos que, ... etc
-
value: o valor do campo - por exemplo: john, 25, ... etc
5. MyUserRepository
Agora - vamos dar uma olhada em nossoMyUserRepository.
Precisamos de nossoMyUserRepository para estenderQuerydslPredicateExecutor para que possamos usarPredicates mais tarde para filtrar os resultados da pesquisa:
public interface MyUserRepository extends JpaRepository,
QuerydslPredicateExecutor, QuerydslBinderCustomizer {
@Override
default public void customize(
QuerydslBindings bindings, QMyUser root) {
bindings.bind(String.class)
.first((SingleValueBinding) StringExpression::containsIgnoreCase);
bindings.excluding(root.email);
}
}
6. CombinePredicates
Em seguida - vamos dar uma olhada na combinação de Predicados para usar várias restrições na filtragem de resultados.
No exemplo a seguir - trabalhamos com um construtor -MyUserPredicatesBuilder - para combinarPredicates:
public class MyUserPredicatesBuilder {
private List params;
public MyUserPredicatesBuilder() {
params = new ArrayList<>();
}
public MyUserPredicatesBuilder with(
String key, String operation, Object value) {
params.add(new SearchCriteria(key, operation, value));
return this;
}
public BooleanExpression build() {
if (params.size() == 0) {
return null;
}
List predicates = params.stream().map(param -> {
MyUserPredicate predicate = new MyUserPredicate(param);
return predicate.getPredicate();
}).filter(Objects::nonNull).collect(Collectors.toList());
BooleanExpression result = Expressions.asBoolean(true).isTrue();
for (BooleanExpression predicate : predicates) {
result = result.and(predicate);
}
return result;
}
}
7. Teste as consultas de pesquisa
Em seguida, vamos testar nossa API de pesquisa.
Começaremos inicializando o banco de dados com alguns usuários - para tê-los prontos e disponíveis para teste:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@Rollback
public class JPAQuerydslIntegrationTest {
@Autowired
private MyUserRepository repo;
private MyUser userJohn;
private MyUser userTom;
@Before
public void init() {
userJohn = new MyUser();
userJohn.setFirstName("John");
userJohn.setLastName("Doe");
userJohn.setEmail("[email protected]");
userJohn.setAge(22);
repo.save(userJohn);
userTom = new MyUser();
userTom.setFirstName("Tom");
userTom.setLastName("Doe");
userTom.setEmail("[email protected]");
userTom.setAge(26);
repo.save(userTom);
}
}
A seguir, vamos ver como encontrar usuários comgiven last name:
@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe");
Iterable results = repo.findAll(builder.build());
assertThat(results, containsInAnyOrder(userJohn, userTom));
}
Agora, vamos ver como encontrar um usuário com determinadoboth first and last name:
@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
.with("firstName", ":", "John").with("lastName", ":", "Doe");
Iterable results = repo.findAll(builder.build());
assertThat(results, contains(userJohn));
assertThat(results, not(contains(userTom)));
}
A seguir, vamos ver como encontrar o usuário com determinadoboth last name and minimum age
@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
.with("lastName", ":", "Doe").with("age", ">", "25");
Iterable results = repo.findAll(builder.build());
assertThat(results, contains(userTom));
assertThat(results, not(contains(userJohn)));
}
Agora, vamos ver como pesquisar porMyUser quedoesn’t actually exist:
@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
.with("firstName", ":", "Adam").with("lastName", ":", "Fox");
Iterable results = repo.findAll(builder.build());
assertThat(results, emptyIterable());
}
Finalmente - vamos ver como encontrar umMyUsergiven only part of the first name - como no exemplo a seguir:
@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo");
Iterable results = repo.findAll(builder.build());
assertThat(results, contains(userJohn));
assertThat(results, not(contains(userTom)));
}
8. UserController
Finalmente, vamos juntar tudo e construir a API REST.
Estamos definindo umUserController que define um método simplesfindAll() com um parâmetro “search“ para passar na string de consulta:
@Controller
public class UserController {
@Autowired
private MyUserRepository myUserRepository;
@RequestMapping(method = RequestMethod.GET, value = "/myusers")
@ResponseBody
public Iterable search(@RequestParam(value = "search") String search) {
MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder();
if (search != null) {
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));
}
}
BooleanExpression exp = builder.build();
return myUserRepository.findAll(exp);
}
}
Aqui está um exemplo rápido de URL de teste:
http://localhost:8080/myusers?search=lastName:doe,age>25
E a resposta:
[{
"id":2,
"firstName":"tom",
"lastName":"doe",
"email":"[email protected]",
"age":26
}]
9. Conclusão
Este terceiro artigo cobriuthe first steps of building a query language for a REST API, fazendo bom uso da biblioteca Querydsl.
A implementação é clara desde o início, mas pode ser facilmente evoluída para dar suporte a operações adicionais.
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á.