Criteria API
Learn how to build queries programmatically using Hibernate's Criteria API. This approach is useful for constructing dynamic queries based on runtime conditions. We'll cover the basics of building criteria queries and executing them.
Hibernate Aggregations and Grouping
Introduction
This document explains how to perform aggregations and grouping using Hibernate, focusing on the GROUP BY
and HAVING
clauses, as well as common aggregate functions like COUNT
, SUM
, AVG
, MIN
, and MAX
. We'll cover how to implement these concepts using Hibernate's Criteria API.
Aggregations and the GROUP BY
Clause
Aggregations are operations that summarize data from multiple rows into a single row. The GROUP BY
clause is used to group rows that have the same values in one or more columns, allowing aggregate functions to operate on these groups.
In essence, GROUP BY
lets you categorize your data based on specific columns, and then calculate aggregate values for each category.
Example Scenario
Imagine you have a table named Order
with columns like orderId
, customerId
, amount
, and orderDate
. You might want to find the total amount spent by each customer. This requires grouping the orders by customerId
and then summing the amount
for each group.
Aggregate Functions
These are the most commonly used aggregate functions:
COUNT()
: Returns the number of rows that match a specified criterion.SUM()
: Returns the sum of numeric values in a column.AVG()
: Returns the average of numeric values in a column.MIN()
: Returns the smallest value in a column.MAX()
: Returns the largest value in a column.
Implementing Aggregations with Hibernate Criteria API
Hibernate provides the Criteria API (and its modern replacement, the Jakarta Persistence Criteria API) for building dynamic queries. Here's how to use it for aggregations:
import jakarta.persistence.criteria.*;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.List;
public class HibernateAggregationExample {
public static void main(String[] args) {
Configuration configuration = new Configuration().configure("hibernate.cfg.xml")
.addAnnotatedClass(Order.class); // Replace Order.class with your entity class
SessionFactory sessionFactory = configuration.buildSessionFactory();
try (Session session = sessionFactory.openSession()) {
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Object[]> query = cb.createQuery(Object[].class);
Root<Order> root = query.from(Order.class); // Replace Order.class with your entity class
// Aggregate Function: SUM(amount) and GROUP BY customerId
Expression<Double> sumOfAmount = cb.sum(root.get("amount")); // "amount" is the property name in your Order entity
query.multiselect(root.get("customerId"), sumOfAmount); // Select customerId and the sum of amounts
query.groupBy(root.get("customerId")); // Group by customerId
List<Object[]> results = session.createQuery(query).getResultList();
System.out.println("Results (Customer ID, Total Amount):");
for (Object[] result : results) {
System.out.println("Customer ID: " + result[0] + ", Total Amount: " + result[1]);
}
// Example using COUNT
CriteriaQuery<Object[]> countQuery = cb.createQuery(Object[].class);
Root<Order> countRoot = countQuery.from(Order.class);
Expression<Long> countOfOrders = cb.count(countRoot);
countQuery.multiselect(countRoot.get("customerId"), countOfOrders);
countQuery.groupBy(countRoot.get("customerId"));
List<Object[]> countResults = session.createQuery(countQuery).getResultList();
System.out.println("\\nResults (Customer ID, Order Count):");
for (Object[] result : countResults) {
System.out.println("Customer ID: " + result[0] + ", Order Count: " + result[1]);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
sessionFactory.close();
}
}
}
// Example Order Entity (You'll need to define your actual entity)
import jakarta.persistence.*;
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
private Long customerId;
private Double amount;
// Getters and setters (omitted for brevity)
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Double getAmount() {
return amount;
}
public void setAmount(Double amount) {
this.amount = amount;
}
}
Explanation:
- Create Criteria Builder and Query:
CriteriaBuilder cb = session.getCriteriaBuilder();
andCriteriaQuery<Object[]> query = cb.createQuery(Object[].class);
create the necessary objects for building the query.Object[].class
is used because we're selecting multiple values (customerId and the aggregated amount). - Create Root:
Root<Order> root = query.from(Order.class);
specifies the entity (table) we're querying from. Replace `Order.class` with your entity class. - Define Aggregate Function:
Expression<Double> sumOfAmount = cb.sum(root.get("amount"));
defines theSUM
aggregate function on the "amount" property of the `Order` entity. Adjust `"amount"` if your entity property name is different. Change the type to `BigDecimal` or appropriate type of your entity. - Select and Group:
query.multiselect(root.get("customerId"), sumOfAmount);
selects thecustomerId
and the calculated sum.query.groupBy(root.get("customerId"));
groups the results bycustomerId
. - Execute Query:
List<Object[]> results = session.createQuery(query).getResultList();
executes the query and retrieves the results. Each element in the list is anObject[]
, whereresult[0]
is thecustomerId
andresult[1]
is the total amount.
Important Notes:
- Remember to replace
Order.class
and property names like"amount"
and"customerId"
with your actual entity class and property names. - Handle exceptions appropriately in a real-world application.
- Make sure your
hibernate.cfg.xml
is correctly configured. - The example above shows how to sum and group the data by a column, similar steps can be used to implement count, min, max, and average aggregations.
The HAVING
Clause
The HAVING
clause is used to filter the results of an aggregation. It's similar to the WHERE
clause, but it operates on grouped data. You can only use the HAVING
clause *after* you've used the GROUP BY
clause. It allows you to filter based on the results of aggregate functions.
For example, you might want to find all customers whose total spending is greater than $1000.
Implementing the HAVING
Clause in Hibernate
import jakarta.persistence.criteria.*;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.List;
public class HibernateHavingExample {
public static void main(String[] args) {
Configuration configuration = new Configuration().configure("hibernate.cfg.xml")
.addAnnotatedClass(Order.class); // Replace Order.class with your entity class
SessionFactory sessionFactory = configuration.buildSessionFactory();
try (Session session = sessionFactory.openSession()) {
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Object[]> query = cb.createQuery(Object[].class);
Root<Order> root = query.from(Order.class); // Replace Order.class with your entity class
// Aggregate Function: SUM(amount) and GROUP BY customerId
Expression<Double> sumOfAmount = cb.sum(root.get("amount")); // "amount" is the property name in your Order entity
query.multiselect(root.get("customerId"), sumOfAmount); // Select customerId and the sum of amounts
query.groupBy(root.get("customerId")); // Group by customerId
// HAVING clause: filter groups where SUM(amount) > 1000
query.having(cb.gt(sumOfAmount, 1000.0));
List<Object[]> results = session.createQuery(query).getResultList();
System.out.println("Results (Customer ID, Total Amount) - Total Amount > 1000:");
for (Object[] result : results) {
System.out.println("Customer ID: " + result[0] + ", Total Amount: " + result[1]);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
sessionFactory.close();
}
}
}
// Example Order Entity (You'll need to define your actual entity)
import jakarta.persistence.*;
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
private Long customerId;
private Double amount;
// Getters and setters (omitted for brevity)
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Double getAmount() {
return amount;
}
public void setAmount(Double amount) {
this.amount = amount;
}
}
Explanation of the HAVING
Clause Example:
- The code is very similar to the previous example up to the
groupBy
clause. - Add
HAVING
Clause:query.having(cb.gt(sumOfAmount, 1000.0));
adds theHAVING
clause.cb.gt(sumOfAmount, 1000.0)
means "select only the groups where the sum of the amount is greater than 1000.0". - The rest of the code is the same: execute the query and print the results.
Key Takeaway: The HAVING
clause filters based on aggregated values, while the WHERE
clause filters individual rows before aggregation.
Alternatives to Criteria API (HQL/JPQL)
While the Criteria API provides type safety, some developers prefer using HQL (Hibernate Query Language) or JPQL (Java Persistence Query Language) for aggregations. These approaches use string-based queries.
Example using JPQL:
import jakarta.persistence.*;
import java.util.List;
public class JpqlAggregationExample {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("yourPersistenceUnitName"); // Replace with your persistence unit name
EntityManager em = emf.createEntityManager();
try {
String jpql = "SELECT o.customerId, SUM(o.amount) FROM Order o GROUP BY o.customerId HAVING SUM(o.amount) > 1000";
TypedQuery<Object[]> query = em.createQuery(jpql, Object[].class);
List<Object[]> results = query.getResultList();
System.out.println("Results (Customer ID, Total Amount) - Total Amount > 1000 (JPQL):");
for (Object[] result : results) {
System.out.println("Customer ID: " + result[0] + ", Total Amount: " + result[1]);
}
} finally {
em.close();
emf.close();
}
}
}
// Example Order Entity (You'll need to define your actual entity)
import jakarta.persistence.*;
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
private Long customerId;
private Double amount;
// Getters and setters (omitted for brevity)
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Double getAmount() {
return amount;
}
public void setAmount(Double amount) {
this.amount = amount;
}
}
Important Considerations when using JPQL/HQL:
- You'll need to set up an
EntityManagerFactory
andEntityManager
instead of a HibernateSessionFactory
andSession
. - Use your persistence unit name in
Persistence.createEntityManagerFactory()
. - The query string is susceptible to SQL injection if you're not careful about handling user input. Parameterization is essential for security.
Choosing the Right Approach
The Criteria API offers type safety and can be useful for building dynamic queries. HQL/JPQL provides a more concise way to express complex queries when type safety is less of a concern, and query complexity demands ease of expression. Choose the approach that best suits your project's requirements and coding style. For new projects, Jakarta Persistence Criteria API is recommended as a modern replacement for the legacy Hibernate Criteria API.