Criando uma camada de projeção mais limpa sobre a API de Criteria do JPA
Se você desenvolve sistemas utilizando Java com JPA, provavelmente já passou pela necessidade de realizar consultas retornando apenas alguns atributos de uma determinada entidade.
À primeira vista, isso pode parecer um problema simples. Porém, quando não tratado da forma correta, com o tempo o sistema tende a acumular consultas desnecessárias ou sobrecarregadas com atributos que não serão utilizados.
Em muitos cenários reais, o desenvolvedor precisa buscar apenas o “id” e o “nome” de uma entidade. No entanto, por conta do tamanho e da complexidade do sistema, acaba sendo difícil identificar se já existe uma consulta que retorne exatamente esse resultado. Em outros casos, acaba-se reutilizando métodos que carregam a entidade completa, apenas para extrair posteriormente os dados realmente necessários.
É nesse contexto que surge o ProjectionQuery. Seu objetivo é simplificar e organizar consultas baseadas em projeção, oferecendo uma forma mais clara e expressiva de selecionar apenas os dados que a aplicação realmente precisa.
Como o ProjectionQuery funciona na prática
Após adicionar a dependência ao seu projeto, você só precisa criar um class(ou um record) e definir quais campos devem ser projetados.
@Projection(of = Customer.class)
public record CustomerBasicData(
@ProjectionField Long id,
@ProjectionField String name,
@ProjectionField("address.city.name") String city,
@ProjectionField("address.city.state.name") String state
) { }
Esta classe atua como a representação final da consulta. Independentemente de quantos atributos existam na entidade Customer, apenas os campos id, name, city e state serão selecionados do banco de dados.
Note que city e state são atributos aninhados, recuperados por meio de relacionamentos definidos na entidade Customer.
O SQL final (simplificado) gerado para esta consulta será semelhante ao seguinte:
select
id,
name,
city.name,
state.name,
from customer
inner join address on customer.address = address.id
inner join city on address.city = city.id
inner join state on city.state = state.id
Agora que definimos nossa classe de projeção, podemos usar o ProjectionProcessor, que atua como mecanismo de execução para as consultas.
Existem duas maneiras principais de executar esta consulta:
Nos exemplos a seguir, assumimos um contexto JPA no qual o serviço
ProjectionProcessorrecebe uma instância deEntityManager, responsável por executar as consultas.
Execução simplificada
ProjectionProcessor processor = new ProjectionProcessor(entityManager);
List<CustomerBasicData> customers = processor.execute(CustomerBasicData.class);
Usando a classe ProjectionQuery
A classe ProjectionQuery ajuda a construir consultas mais avançadas, permitindo a adição de filtros, ordenação, paginação e outras configurações.
ProjectionProcessor processor = new ProjectionProcessor(entityManager);
ProjectionQuery<Customer, CustomerBasicData> query = ProjectionQuery
.fromTo(Customer.class, CustomerBasicData.class)
.filter("address.city.name", ProjectionFilterOperator.EQUAL, "São Paulo")
.order("name", OrderDirection.ASC)
.paging(0, 20)
.distinct();
List<CustomerBasicData> customers = processor.execute(query);
ProjectionQuery pode ser usado tanto de forma independente quanto integrado em aplicações Spring Boot.
Para obter mais detalhes, exemplos adicionais e documentação completa, consulte a página do projeto no GitHub.