Persistence top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll learn how to project entity properties using JPA and Hibernate.

2. The Entity

First, let’s look at the entity we will be using throughout this article:

@Entity
public class Product {
    @Id
    private long id;
    
    private String name;
    
    private String description;
    
    private String category;
    
    private BigDecimal unitPrice;

    // setters and getters
}

This is a simple entity class representing a product with various properties.

3. JPA Projections

Though the JPA spec doesn’t mention projections explicitly, there are many cases where we find them in concept.

Typically, a JPQL query has a candidate entity class. The query, on execution, creates objects of the candidate class — populating all their properties using the data retrieved.

But, it’s possible to retrieve a subset of the properties of the entity, or, that is, a projection of column data.

Apart from column data, we can also project the results of grouping functions.

3.1. Single-Column Projections

Let’s suppose we want to list the names of all products. In JPQL, we can do this by including only the name in the select clause:

Query query = entityManager.createQuery("select name from Product");
List<Object> resultList = query.getResultList();

Or, we can do the same with CriteriaBuilder:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<String> query = builder.createQuery(String.class);
Root<Product> product = query.from(Product.class);
query.select(product.get("name"));
List<String> resultList = entityManager.createQuery(query).getResultList();

Because we are projecting a single column that happens to be of type String, we expect to get a list of Strings in the result. Hence, we’ve specified the candidate class as String in the createQuery() method.

Since we want to project on a single property, we’ve used the Query.select() method. What goes here is which property we want, so in our case, we’d use the name property from our Product entity.

Now, let’s look at a sample output generated by the above two queries:

Product Name 1
Product Name 2
Product Name 3
Product Name 4

Note that if we’d used the id property in the projection instead of name, the query would have returned a list of Long objects.

3.2. Multi-Column Projections

To project on multiple columns using JPQL, we only have to add all the required columns to the select clause:

Query query = session.createQuery("select id, name, unitPrice from Product");
List resultList = query.getResultList();

But, when using a CriteriaBuilder, we’ll have to do things a bit differently:

CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Object[]> query = builder.createQuery(Object[].class);
Root<Product> product = query.from(Product.class);
query.multiselect(product.get("id"), product.get("name"), product.get("unitPrice"));
List<Object[]> resultList = entityManager.createQuery(query).getResultList();

Here, we’ve used the method multiselect() instead of select(). Using this method, we can specify multiple items to be selected.

Another significant change is the use of Object[]. When we select multiple items, the query returns an object array with value for each item projected. This is the case with JPQL as well.

Let’s see what the data looks like when we print it:

[1, Product Name 1, 1.40]
[2, Product Name 2, 4.30]
[3, Product Name 3, 14.00]
[4, Product Name 4, 3.90]

As we can see, the returned data is a bit cumbersome to process. But, fortunately, we can get JPA to populate this data into a custom class.

Also, we can use CriteriaBuilder.tuple() or CriteriaBuilder.construct() to get the results as a list of Tuple objects or objects of a custom class respectively.

3.3. Projecting Aggregate Functions

Apart from column data, we might sometimes want to group the data and use aggregate functions, like count and average.

Let’s say we want to find the number of products in each category. We can do this using the count() aggregate function in JPQL:

Query query = entityManager.createQuery("select p.category, count(p) from Product p group by p.category");

Or we can use CriteriaBuilder:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object[]> query = builder.createQuery(Object[].class);
Root<Product> product = query.from(Product.class);
query.multiselect(product.get("category"), builder.count(product));
query.groupBy(product.get("category"));

Here, we’ve used CriteriaBuilder‘s count() method.

Using either of the above will produce a list of object arrays:

[category1, 2]
[category2, 1]
[category3, 1]

Apart from count(), CriteriaBuilder provides various other aggregate functions:

  • avg – Calculates the average value for a column in a group
  • max – Calculates the maximum value for a column in a group
  • min – Calculates the minimum value for a column in a group
  • least – Finds the least of the column values (for example, alphabetically or by date)
  • sum – Calculates the sum of the column values in a group

4. Hibernate Projections

Unlike JPA, Hibernate provides org.hibernate.criterion.Projection for projecting with a Criteria query. It also provides a class called org.hibernate.criterion.Projections, a factory for Projection instances.

4.1. Single-Column Projections

First, let’s see how we can project a single column. We’ll use the example we saw earlier:

Criteria criteria = session.createCriteria(Product.class);
criteria = criteria.setProjection(Projections.property("name"));

We’ve used the Criteria.setProjection() method to specify the property that we want in the query result. Projections.property() does the same work for us as Root.get() did when indicating the column to select.

4.2. Multi-Column Projections

To project multiple columns, we’ll have to first create a ProjectionList. ProjectionList is a special kind of Projection that wraps other projections to allow selecting multiple values.

We can create a ProjectionList using the Projections.projectionList() method, like showing the Product‘s id and name:

Criteria criteria = session.createCriteria(Product.class);
criteria = criteria.setProjection(
  Projections.projectionList()
    .add(Projections.id())
    .add(Projections.property("name")));

4.3. Projecting Aggregate Functions

Just like CriteriaBuilder, the Projections class also provides methods for aggregate functions.

Let’s see how we can implement the count example we saw earlier:

Criteria criteria = session.createCriteria(Product.class);
criteria = criteria.setProjection(
  Projections.projectionList()
    .add(Projections.groupProperty("category"))
    .add(Projections.rowCount()));

It’s important to note that we didn’t directly specify the GROUP BY in the Criteria object. Calling groupProperty triggers this for us.

Apart from the rowCount() function, Projections also provides the aggregate functions we saw earlier.

4.4. Using an Alias for a Projection

An interesting feature of the Hibernate Criteria API is the use of an alias for a projection.

This is especially useful when using an aggregate function, as we can then refer to the alias in the Criterion and Order instances:

Criteria criteria = session.createCriteria(Product.class);
criteria = criteria.setProjection(Projections.projectionList()
             .add(Projections.groupProperty("category"))
             .add(Projections.alias(Projections.rowCount(), "count")));
criteria.addOrder(Order.asc("count"));

5. Conclusion

In this article, we saw how to project entity properties using JPA and Hibernate.

It is important to note that Hibernate has deprecated its Criteria API from version 5.2 onwards in favor of the JPA CriteriaQuery API. But, this is only because the Hibernate team doesn’t have the time to keep two different APIs, that pretty much do the same thing, in sync.

And of course, the code used in this article can be found over on GitHub.

Persistence bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE