Tutorials Logic, IN info@tutorialslogic.com

Hibernate Criteria API Type Safe Dynamic Queries

What is the Criteria API?

The JPA Criteria API provides a type-safe, programmatic way to build queries. Unlike HQL (string-based), Criteria queries are built using Java objects and are checked at compile time - typos in property names cause compile errors, not runtime errors.

The Criteria API is especially useful for building dynamic queries where the conditions are not known at compile time (e.g., search filters).

Criteria API Basics

Criteria API Basics
import jakarta.persistence.criteria.*;

Session session = sessionFactory.openSession();

// Get CriteriaBuilder from session
CriteriaBuilder cb = session.getCriteriaBuilder();

// Create CriteriaQuery for User
CriteriaQuery<User> cq = cb.createQuery(User.class);

// Define the root entity (FROM User u)
Root<User> root = cq.from(User.class);

// SELECT * FROM users (no WHERE)
cq.select(root);
List<User> allUsers = session.createQuery(cq).getResultList();

// WHERE u.role = 'ADMIN'
Predicate roleAdmin = cb.equal(root.get("role"), "ADMIN");
cq.where(roleAdmin);
List<User> admins = session.createQuery(cq).getResultList();

// WHERE u.age >= 18 AND u.active = true
Predicate ageCheck   = cb.greaterThanOrEqualTo(root.get("age"), 18);
Predicate activeCheck = cb.isTrue(root.get("active"));
cq.where(cb.and(ageCheck, activeCheck));

// ORDER BY u.lastName ASC
cq.orderBy(cb.asc(root.get("lastName")));

// Execute
List<User> result = session.createQuery(cq).getResultList();

session.close();

Dynamic Queries and Pagination

Dynamic Queries and Pagination
// Dynamic search query - conditions added based on non-null parameters
public List<User> searchUsers(String name, String email, String role,
                               int page, int pageSize) {
    Session session = sessionFactory.openSession();
    CriteriaBuilder cb = session.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> root = cq.from(User.class);

    List<Predicate> predicates = new ArrayList<>();

    // Add conditions only if parameters are provided
    if (name != null && !name.isEmpty()) {
        predicates.add(cb.like(cb.lower(root.get("username")),
                               "%" + name.toLowerCase() + "%"));
    }
    if (email != null && !email.isEmpty()) {
        predicates.add(cb.like(root.get("email"), "%" + email + "%"));
    }
    if (role != null && !role.isEmpty()) {
        predicates.add(cb.equal(root.get("role"), role));
    }

    // Combine all predicates with AND
    if (!predicates.isEmpty()) {
        cq.where(cb.and(predicates.toArray(new Predicate[0])));
    }

    cq.orderBy(cb.asc(root.get("username")));

    // Pagination
    List<User> results = session.createQuery(cq)
            .setFirstResult(page * pageSize)  // Offset
            .setMaxResults(pageSize)           // Limit
            .getResultList();

    session.close();
    return results;
}

// Count query for pagination
public Long countUsers(String role) {
    Session session = sessionFactory.openSession();
    CriteriaBuilder cb = session.getCriteriaBuilder();
    CriteriaQuery<Long> cq = cb.createQuery(Long.class);
    Root<User> root = cq.from(User.class);

    cq.select(cb.count(root));
    if (role != null) {
        cq.where(cb.equal(root.get("role"), role));
    }

    Long count = session.createQuery(cq).uniqueResult();
    session.close();
    return count;
}

Build Optional Predicates Without Losing SQL Visibility

Criteria API is useful when optional filters, reusable predicates, and typed paths make a query genuinely dynamic.

It is usually less readable than HQL for a fixed query. Use it when runtime choices change joins or predicates, and still inspect the generated SQL so type safety does not hide inefficient execution.

  • Build predicates only for provided filters.
  • Extract reusable Specification-style conditions.
  • Use metamodel or typed paths where available.
  • Keep joins explicit and named in code.
  • Test empty-filter and many-filter cases separately.

Compose Predicates with Explicit Boolean Grouping

Build each validated filter as a Predicate, then combine groups with cb.and or cb.or in the same structure as the business rule. A flat list can silently change A AND (B OR C) into (A AND B) OR C. Treat an absent filter differently from a filter whose value is empty or invalid.

Do not accept entity property names directly from a request. Map supported sort and filter names to known metamodel paths so user input cannot choose an arbitrary field or join.

Joins, Projections, Subqueries, and Distinct Roots

Create joins once and reuse them when several predicates target the same association. Collection joins can duplicate root rows, so use distinct only after understanding the SQL and count-query effect. A DTO or Tuple projection can avoid loading complete entities for a read-only result.

Use a subquery when the condition is naturally an existence or aggregate test, but compare its plan with a join. Type-safe construction does not guarantee efficient SQL; inspect the generated statement and database execution plan.

Stable Pagination and Query Tests

Pagination requires a deterministic order that ends with a unique tie-breaker. Build the count query deliberately because fetch joins and projections from the result query may not belong in it. For deep pages, evaluate keyset pagination when an indexed continuation value is available.

Test no filters, each filter alone, grouped filters, invalid values, no matches, duplicate join rows, and both sort directions. Assert results and query count, then review SQL with realistic cardinality.

Before you move on

Hibernate Criteria API Type Safe Dynamic Queries Mastery Check

4 checks
  • Build paths from the correct Root or Join and keep result types consistent with the CriteriaQuery.
  • Collect optional predicates explicitly so an absent filter does not change another condition.
  • Bind external values and apply deterministic ordering before pagination.
  • Inspect generated SQL, joins, fetch behavior, and query count before choosing Criteria over readable HQL.

Hibernate Questions Learners Ask

Criteria is useful when filters, joins, sorting, and projections are assembled dynamically from optional inputs.

Pagination without a deterministic ORDER BY can return rows in different orders between executions.

Create predicates only for validated, present filter values, then combine them with the intended AND or OR grouping. Empty strings, unbounded date ranges, and user-controlled property names should be normalized before query construction.

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.