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!