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:

  1. Create Criteria Builder and Query: CriteriaBuilder cb = session.getCriteriaBuilder(); and CriteriaQuery<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).
  2. Create Root: Root<Order> root = query.from(Order.class); specifies the entity (table) we're querying from. Replace `Order.class` with your entity class.
  3. Define Aggregate Function: Expression<Double> sumOfAmount = cb.sum(root.get("amount")); defines the SUM 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.
  4. Select and Group: query.multiselect(root.get("customerId"), sumOfAmount); selects the customerId and the calculated sum. query.groupBy(root.get("customerId")); groups the results by customerId.
  5. Execute Query: List<Object[]> results = session.createQuery(query).getResultList(); executes the query and retrieves the results. Each element in the list is an Object[], where result[0] is the customerId and result[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:

  1. The code is very similar to the previous example up to the groupBy clause.
  2. Add HAVING Clause: query.having(cb.gt(sumOfAmount, 1000.0)); adds the HAVING clause. cb.gt(sumOfAmount, 1000.0) means "select only the groups where the sum of the amount is greater than 1000.0".
  3. 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 and EntityManager instead of a Hibernate SessionFactory and Session.
  • 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.