Seguindo o artigo passado, a ideia agora é evoluir as buscas do seu projeto. Já vi em vários lugares APIs que fazem busca por vários e vários atributos de uma classe, mas junto com isso, vi vários e vários if’s encadeados, verificando se tal ou tal atributo é nulo ou foi passado para a busca.
O que esse artigo quer mostrar é como você pode deixar sua ORM buscável com dois níveis de profundidade de objetos utilizando reflexão e fazendo isso de uma maneira simples e de fácil manutenção e reutilização.
Bom, então, chega de muito papo e vamos mostrar como fazer isso. Dessa, vez eu vou começar da API Rest para a base de dados, o exemplo do artigo anterior tem uma classe chamada PersonRestService e o conteúdo dela está assim:
@RestController
@RequestMapping("/persons")
public class PersonRestService {
@Autowired
private PersonService personService;
@GetMapping
public Page<Person> list(@RequestParam(required = false) String name,
@RequestParam(required = false) Integer cpf,
@RequestParam(required = false) Integer phone,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return personService.list(name, cpf, phone, new PageRequest(page, size));
}
}
A princípio, o código está correto e não existe problema algum, mas caso seja necessário adicionar mais um filtro e mais um e mais um… A coisa fica feia! Vai ser preciso adicionar mais parâmetros ao método, então, vamos mudar para receber um Map<String, String>, assim a API será capaz de receber qualquer atributo de request e o código irá ficar assim:
@RestController
@RequestMapping("/persons")
public class PersonRestService {
@Autowired
private PersonService personService;
@GetMapping
public Page<Person> list(@RequestParam(required = false) Map<String, String> filters,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return personService.list(filters, new PageRequest(page, size));
}
}
Agora que a API Rest já está preparada, é hora de preparar o serviço para receber o Map de filtros, a classe de serviço PersonServiceProvider antes era assim:
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
private PersonRepository personRepository;
@Override
public Page<Person> list(String name, Integer cpf, Integer phone, Pageable pageable) {
return personRepository.findAll(where(PersonSpecification.name(name))
.or(PersonSpecification.cpf(cpf)).and(PersonSpecification.phone(phone)), pageable);
}
}
Alterando o serviço, ele deve ficar assim:
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
private PersonRepository personRepository;
@Override
public Page<Person> list(Map<String, String> filters, Pageable pageable) {
return personRepository.findAll(filterWithOptions(filters), pageable);
}
}
Agora vem a parte mais importante que é o Specification com o método que faz o filtro de todos os atributos por reflexão e caso ainda seja uma Lista, é feito um Join. O código que faz a “mágica” ficou assim:
public class PersonSpecification {
private static final String FIELD_SEPARATOR = ".";
private static final String REGEX_FIELD_SPLITTER = "\\.";
public static Specification<Person> filterWithOptions(final Map<String, String> params) {
return (root, query, criteriaBuilder) -> {
try {
List<Predicate> predicates = new ArrayList<>();
for (String field : params.keySet()) {
if (field.contains(FIELD_SEPARATOR)) {
filterInDepth(params, root, criteriaBuilder, predicates, field);
} else {
if (Person.class.getDeclaredField(field) != null) {
predicates.add(criteriaBuilder.equal(root.get(field), params.get(field)));
}
}
}
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
};
}
private static void filterInDepth(Map<String, String> params, Root<Person> root, CriteriaBuilder criteriaBuilder,
List<Predicate> predicates, String field) throws NoSuchFieldException {
String[] compositeField = field.split(REGEX_FIELD_SPLITTER);
if (compositeField.length == 2) {
if(Collection.class.isAssignableFrom(Person.class.getDeclaredField(compositeField[0]).getType())) {
Join<Object, Object> join = root.join(compositeField[0]);
predicates.add(criteriaBuilder.equal(join.get(compositeField[1]), params.get(field)));
}
} else if(Person.class.getDeclaredField(compositeField[0]).getType().getDeclaredField(compositeField[1]) != null) {
predicates.add(criteriaBuilder.equal(root.get(compositeField[0]).get(compositeField[1]), params.get(field)));
}
}
}
Pronto, agora você pode fazer a buscas do tipo /persons?name=Name e, além disso, é possível realizar buscas como /persons?address.street mesmo que address seja uma lista. Além disso, o código pode ser evoluído com a criação de uma classe Abstrata e depois só é preciso estender essa classe e seu Specification terá essa capacidade.
Deixem dúvidas e comentários.
Valeu e até a próxima!



