Building a Cleaner Projection Layer on Top of JPA Criteria API
Source: Dev.to
Introduction
If you develop systems using Java with JPA, you have probably faced the need to execute queries that return only a subset of attributes from a given entity.
At first glance this may seem simple, but when not handled properly systems can accumulate unnecessary queries or queries overloaded with attributes that will never be used.
In many real‑world scenarios a developer needs to retrieve only the id and name of an entity. Because of the size and complexity of the system, it often becomes difficult to identify whether a query already exists that returns exactly this data. In other cases developers end up reusing methods that load the entire entity, only to later extract the few attributes that are actually required.
ProjectionQuery was created to simplify and organize projection‑based queries, providing a clearer and more expressive way to select only the data the application truly needs.
Defining a Projection
After adding the dependency to your project, create a class (or a record) and annotate the fields that should be projected.
@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
) { }
This record represents the final shape of the query result. Regardless of how many attributes exist in the Customer entity, only id, name, city, and state will be selected from the database.
Note that city and state are nested attributes retrieved through relationships defined in the Customer entity.
Generated SQL (simplified)
The SQL generated for the above projection looks roughly like this:
SELECT
c.id,
c.name,
city.name AS city,
state.name AS state
FROM customer c
INNER JOIN address a ON c.address = a.id
INNER JOIN city ci ON a.city = ci.id
INNER JOIN state s ON ci.state = s.id;
Executing Projections
Simplified execution
ProjectionProcessor processor = new ProjectionProcessor(entityManager);
List customers = processor.execute(CustomerBasicData.class);
Using ProjectionQuery for advanced scenarios
ProjectionQuery helps build more sophisticated queries, allowing the addition of filters, sorting, pagination, and other configurations.
ProjectionProcessor processor = new ProjectionProcessor(entityManager);
ProjectionQuery 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 customers = processor.execute(query);
ProjectionQuery can be used both independently and integrated into Spring Boot applications.
Further Resources
For more details, additional examples, and full documentation, please refer to the project page on GitHub.