APIs e Microsserviços

11 jul, 2017

Buscando por todos os campos com reflexão

Publicidade

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!