Projections and Result Transformations in JPA Criteria Queries
When you’re working with JPA (Java Persistence API) to get info from a database, you don’t always need everything. Sometimes, you just want only a certain parts of data, like special columns. That’s where JPA’s projections and result changes come in to picture. In this blog post, we’ll explore these ideas and see how they can help you get the data you need and adjust your search results to fit what you want.
Understanding Projections
Projections in JPA criteria queries refer to the ability to select specific columns or attributes from entities, rather than fetching the entire entity. This can significantly improve query performance and reduce unnecessary data transfer. The Criteria API provides a flexible way to achieve this by using the CriteriaBuilder
and CriteriaQuery
interfaces.
Let’s consider a scenario with two entities: Customer
and Order
.
@Entity
public class Customer {
@Id
@GeneratedValue
private Long id;
private String firstName;
private String lastName;
// Other fields, getters, and setters
}
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@ManyToOne
private Customer customer;
private LocalDate orderDate;
// Other fields, getters, and setters
}
Suppose you want to retrieve only the names of customers who have placed orders. Here’s how you can achieve this using projections:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> query = cb.createQuery(String.class);
Root<Order> orderRoot = query.from(Order.class);
Join<Order, Customer> customerJoin = orderRoot.join("customer");
query.select(customerJoin.get("firstName"));
List<String> customerNames = entityManager.createQuery(query).getResultList();
Equivalent SQL Query:
SELECT c.first_name
FROM order o
JOIN customer c ON o.customer_id = c.id;
In this example, customerJoin.get("firstName")
specifies the projection, indicating that only the firstName
attribute of the Customer
entity should be retrieved.
Result Transformations
While projections help in fetching specific attributes, sometimes you might need to transform the query results into custom Java objects that hold a subset of data from multiple entities. This is particularly useful when you want to create DTOs (Data Transfer Objects) or encapsulate calculated values from the query.
Continuing with our Customer
and Order
entities, let’s say you want to retrieve a list of objects containing customer names and the total number of orders they’ve placed. Here’s how result transformations can be used:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Object[]> query = cb.createQuery(Object[].class);
Root<Order> orderRoot = query.from(Order.class);
Join<Order, Customer> customerJoin = orderRoot.join("customer");
query.multiselect(customerJoin.get("firstName"), cb.count(orderRoot));
query.groupBy(customerJoin.get("firstName"));
List<Object[]> results = entityManager.createQuery(query).getResultList();
List<CustomerOrderSummary> summaries = new ArrayList<>();
for (Object[] result : results) {
summaries.add(new CustomerOrderSummary((String) result[0], (Long) result[1]));
}
Equivalent SQL Query:
SELECT c.first_name, COUNT(o.id)
FROM order o
JOIN customer c ON o.customer_id = c.id
GROUP BY c.first_name;
In this example, we’ve defined a class CustomerOrderSummary
to hold the transformation result:
public class CustomerOrderSummary {
private String customerName;
private Long orderCount;
public CustomerOrderSummary(String customerName, Long orderCount) {
this.customerName = customerName;
this.orderCount = orderCount;
}
// Getters and setters
}
By using the multiselect
method, we’ve specified the attributes we want to retrieve and transform in the query results. The groupBy
clause groups the results by customer names.