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 Projections (SELECT Clause)

In Hibernate, projections are used to specify which columns or calculated values you want to retrieve from the database. They allow you to select specific properties or aggregated values instead of retrieving entire entities. This is similar to the SELECT clause in SQL and can significantly improve performance, especially when dealing with large tables or when you only need a subset of the data.

Why Use Projections?

  • Performance Improvement: Reduces the amount of data transferred from the database server to the application.
  • Custom Data Structures: Allows you to create custom objects or data structures directly from query results.
  • Data Aggregation: Enables you to perform calculations like sums, averages, counts, etc., directly in the database.
  • Code Clarity: Makes your queries more focused and easier to understand.

Using Projections in Hibernate Criteria API

The Hibernate Criteria API provides a flexible way to use projections. Here's how:

Basic Projection: Selecting a Single Property

To select a single property (e.g., the name of a Product entity), you can use the Projections.property() method.

Example

 import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;

import java.util.List;

public class ProjectionExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) { // Replace with your SessionFactory initialization
            Criteria criteria = session.createCriteria(Product.class);
            criteria.setProjection(Projections.property("name")); // Select only the 'name' property

            List<String> names = criteria.list();

            for (String name : names) {
                System.out.println("Product Name: " + name);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

Explanation:

  • We create a Criteria object for the Product entity.
  • Projections.property("name") specifies that we only want to retrieve the name property.
  • The criteria.list() method returns a List<String>, where each element is the name of a product.

Selecting Multiple Properties: Using Tuple or List

To select multiple properties, you can use Projections.projectionList() along with Projections.property() for each property. The result will be a list of object arrays, where each array contains the selected properties in the specified order. Alternatively, you can configure Hibernate to return results as Tuples (requires Hibernate 5.2+ and database support).

Example (List of Object Arrays)

 import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.transform.Transformers;

import java.util.List;

public class ProjectionExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) { // Replace with your SessionFactory initialization
            Criteria criteria = session.createCriteria(Product.class);
            criteria.setProjection(Projections.projectionList()
                    .add(Projections.property("id"))  // Order matters!
                    .add(Projections.property("name"))
                    .add(Projections.property("price")));

            List<Object[]> results = criteria.list();

            for (Object[] result : results) {
                Long id = (Long) result[0];
                String name = (String) result[1];
                Double price = (Double) result[2];
                System.out.println("Product ID: " + id + ", Name: " + name + ", Price: " + price);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

Explanation:

  • We create a ProjectionList and add the desired properties (id, name, price). The order in which you add the properties determines the order of the elements in the resulting array.
  • The criteria.list() method returns a List<Object[]>. Each Object[] represents a row, and its elements are the values of the selected properties.
  • We then iterate through the list and cast each element of the array to its corresponding data type.

Example (Using ResultTransformer to a Custom Class - DTO)

 import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.transform.Transformers;
import java.util.List;

// Assuming you have a ProductDTO class with fields for id, name, and price and a constructor
public class ProjectionExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) {
            Criteria criteria = session.createCriteria(Product.class);
            criteria.setProjection(Projections.projectionList()
                    .add(Projections.property("id"), "id")  // Alias the properties
                    .add(Projections.property("name"), "name")
                    .add(Projections.property("price"), "price"));

            criteria.setResultTransformer(Transformers.aliasToBean(ProductDTO.class));

            List<ProductDTO> productDTOs = criteria.list();

            for (ProductDTO product : productDTOs) {
                System.out.println("Product ID: " + product.getId() + ", Name: " + product.getName() + ", Price: " + product.getPrice());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// Example ProductDTO class
class ProductDTO {
    private Long id;
    private String name;
    private Double price;

    public ProductDTO() {}

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
} 

Explanation:

  • This example shows how to transform the results of the projection into a custom DTO (Data Transfer Object).
  • We use Projections.property("propertyName", "alias") to assign an alias to each property we are selecting. These aliases must match the field names in the DTO class.
  • Transformers.aliasToBean(ProductDTO.class) tells Hibernate to use the aliases and field names to map the result set into instances of ProductDTO.
  • The resulting List now contains ProductDTO objects, making it easier to work with the data.
  • A no-argument constructor is required in the DTO for this to work.

Aggregate Functions

Hibernate supports various aggregate functions like count, sum, avg, min, and max. These can be used within projections to perform calculations directly in the database.

Example: Calculating the Average Price

 import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;

public class ProjectionExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) { // Replace with your SessionFactory initialization
            Criteria criteria = session.createCriteria(Product.class);
            criteria.setProjection(Projections.avg("price")); // Calculate the average price

            Double averagePrice = (Double) criteria.uniqueResult();

            System.out.println("Average Price: " + averagePrice);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

Explanation:

  • Projections.avg("price") calculates the average value of the price property.
  • criteria.uniqueResult() returns a single result, which is the average price in this case.

Using `Restrictions` with Projections

You can combine projections with restrictions (using the Restrictions class) to filter the data before applying the projections.

Example: Finding the Names of Products with a Price Greater Than 100

 import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;

import java.util.List;

public class ProjectionExample {

    public static void main(String[] args) {
        try (Session session = HibernateUtil.getSessionFactory().openSession()) { // Replace with your SessionFactory initialization
            Criteria criteria = session.createCriteria(Product.class);
            criteria.add(Restrictions.gt("price", 100.0)); // Filter products with price > 100
            criteria.setProjection(Projections.property("name")); // Select only the name

            List<String> names = criteria.list();

            for (String name : names) {
                System.out.println("Product Name: " + name);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

Explanation:

  • Restrictions.gt("price", 100.0) adds a restriction to only select products where the price is greater than 100.
  • The projection (Projections.property("name")) is then applied to the filtered results.

Important Considerations

  • Data Types: Ensure that you are using the correct data types when casting the results of projections.
  • DTOs and Transformers: Using DTOs and result transformers makes your code more maintainable and easier to understand, especially when dealing with complex projections.
  • Performance: Always analyze your queries and use projections effectively to optimize performance. Overusing projections or selecting too many properties can negate the benefits.
  • `uniqueResult()`: When using aggregate functions or projections that return a single value, use criteria.uniqueResult() to retrieve the result directly.