Criteria: Predicates & Dynamic Queries
Criteria: Predicates & Dynamic Queries
The previous lesson introduced the Criteria API and showed how to build a basic typed query. The real payoff of the API, however, is its ability to compose query conditions at runtime. When a user can filter a product list by name, category, minimum price, maximum price, or any combination of those, a static JPQL string simply cannot express every possibility. Criteria predicates are the building blocks that solve this problem.
What Is a Predicate?
In the Criteria API, a Predicate represents a boolean SQL expression — anything that could appear in a WHERE clause or as a join condition. The CriteriaBuilder is the factory that creates predicates, and you can combine them with logical operators to model arbitrarily complex conditions.
Common builder methods that produce predicates:
cb.equal(expr, value)— equality testcb.like(expr, pattern)— SQLLIKEcb.greaterThanOrEqualTo(expr, value)/cb.lessThanOrEqualTo(expr, value)cb.between(expr, lower, upper)cb.isNull(expr)/cb.isNotNull(expr)cb.in(expr).value(v1).value(v2)— SQLINcb.and(p1, p2, ...))/cb.or(p1, p2, ...)— logical combinationcb.not(predicate)— negation
Building a Simple Predicate
Assume an Order entity with fields status, totalAmount, and a many-to-one relationship to Customer. A query for orders with status SHIPPED and total above a threshold looks like this:
Each predicate is an independent object. You combine them by passing them to cb.and() before handing the result to cq.where().
Dynamic Query Composition
The key pattern for search forms is to accumulate predicates into a list and only add a predicate when the corresponding filter value is actually present. This keeps the generated SQL minimal — no spurious AND 1=1 tricks required.
where(): When no filters are provided, predicates is empty and toArray produces a zero-length array. JPA treats where(new Predicate[0]) as no restriction — equivalent to omitting the WHERE clause entirely. This means the same method gracefully handles both "return everything" and fully filtered queries.
OR Conditions and Nested Logic
Mix cb.and() and cb.or() to model compound conditions. Suppose you want orders that are either PENDING or past-due (status OVERDUE), and whose amount exceeds a minimum:
This compiles to:
LIKE and Case-Insensitive Search
Full-text substring matching is a common requirement. Wrap the column expression with cb.lower() and lowercase the search string in Java to achieve portable case-insensitive matching:
%term) disables B-tree index scans on most databases. For production full-text search on large tables prefer a dedicated solution (PostgreSQL tsvector, Elasticsearch, or a full-text index) rather than LIKE '%...'. Reserve leading-wildcard LIKE for admin panels and low-volume searches.
Subqueries as Predicates
The Criteria API supports correlated subqueries through cq.subquery(). This is useful for "exists" conditions — for example, returning only customers who have at least one shipped order:
The Specification Pattern (Spring Data JPA)
Writing all predicate logic inside a repository quickly becomes verbose. Spring Data JPA's Specification<T> interface wraps a single predicate-building lambda into a reusable, composable unit. Enable it by extending JpaSpecificationExecutor<T> in your repository interface:
Define individual specifications as static factory methods in a utility class:
Combine and execute them with a fluent API:
cb.conjunction()? It returns a predicate that is always TRUE, acting as a no-op when a filter value is absent. This lets each specification method handle its own null check internally, so the caller never has to guard against nulls.
Performance Considerations
- Bind parameters, never string concatenation. All Criteria API values are automatically bound as JDBC parameters — SQL injection is not possible, and the database can cache execution plans.
- Avoid overly broad result sets. Dynamic queries can easily return millions of rows if all filters are null. Apply sensible pagination (
setFirstResult/setMaxResults) for any user-facing search. - Fetch joins in dynamic queries. If you conditionally join an association to filter by it, also fetch it to avoid N+1 loads. Use
root.fetch("customer", JoinType.LEFT)when the join is for data loading, androot.join()when it is purely for filtering. - Distinct when fetch-joining collections. Joining a one-to-many inflates the result set. Add
cq.distinct(true)or useDISTINCTin the query to deduplicate.
Summary
Predicates are the atomic units of Criteria API queries. Build them individually with CriteriaBuilder, collect them in a list, and combine them with cb.and() / cb.or() to represent any filter combination a user might choose. The Specification pattern from Spring Data JPA raises this to an even higher level of reusability by encapsulating each predicate as a named, composable object. Together, these techniques replace the fragile string-concatenation approach to dynamic SQL with a type-safe, testable, and readable alternative.