Criteria API and Spring Data Specifications - Type-Safe Queries
Criteria API and Spring Data Specifications - Type-Safe Queries
HQL and JPQL are powerful but still rely on string-based queries. If you misspell a column name or use the wrong type, you find out at runtime. The JPA Criteria API and Spring Data Specifications solve this by letting you build queries programmatically in Java, catching errors at compile time instead of in production.
The Problem with String Queries
Consider a typical JPQL query that filters books by author and minimum price. One typo in the string and you have a runtime exception buried in a log file:
// Fragile - typo in field name causes runtime error
String jpql = "SELECT b FROM Book b WHERE b.authr = :author AND b.price > :minPrice";
TypedQuery<Book> query = entityManager.createQuery(jpql, Book.class);
query.setParameter("author", "Joshua Bloch");
query.setParameter("minPrice", 30.0);
List<Book> results = query.getResultList();
The typo b.authr is invisible to the compiler. The Criteria API turns this into a chain of method calls and typed references that the compiler validates before you ever run the application.
Building a Criteria Query
The Criteria API uses a builder pattern. You obtain a CriteriaBuilder, create a query root, and compose predicates programmatically:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Book> cq = cb.createQuery(Book.class);
Root<Book> book = cq.from(Book.class);
Predicate authorMatch = cb.equal(book.get("author"), "Joshua Bloch");
Predicate priceAbove = cb.greaterThan(book.get("price"), 30.0);
cq.select(book).where(cb.and(authorMatch, priceAbove));
List<Book> results = entityManager.createQuery(cq).getResultList();
cb.createQuery(Book.class) creates a typed query. cq.from(Book.class) returns a Root<Book> that represents the table. book.get("author") returns a Path<String> - if the field does not exist on Book, you get a compile-time error when using metamodel classes (more on that below). The predicates authorMatch and priceAbove are composed with cb.and() and passed to cq.where().
Spring Data Specifications
Spring Data JPA wraps the Criteria API into a simpler abstraction called Specification<T>. You write small, reusable specification objects that can be combined with logical operators:
public class BookSpecs {
public static Specification<Book> hasAuthor(String author) {
return (root, query, cb) -> cb.equal(root.get("author"), author);
}
public static Specification<Book> priceAbove(double minPrice) {
return (root, query, cb) -> cb.greaterThan(root.get("price"), minPrice);
}
}
// Usage in a Spring Data repository
List<Book> results = bookRepository.findAll(
Specification.where(BookSpecs.hasAuthor("Joshua Bloch"))
.and(BookSpecs.priceAbove(30.0))
);
Each specification is a lambda that receives the Root, Query, and CriteriaBuilder. The Specification.where() starts the chain, and .and() combines predicates. This is extremely composable - you can add or remove filters dynamically based on user input without building JPQL strings.
Metamodel for Full Type Safety
The string-based root.get("author") still has a runtime failure risk. JPA metamodel classes (generated from your entities at compile time) eliminate this entirely. Instead of a string, you pass a typed attribute reference:
import static com.example.model.Book_;
Predicate authorMatch = cb.equal(book.get(Book_.author), "Joshua Bloch");
Book_ is a generated metamodel class. If you rename the author field, the code fails to compile. The Hibernate Tools or hibernate-jpamodelgen annotation processor generates these classes automatically during build.
Real-World Scenario
Imagine an e-commerce dashboard where admins can filter products by category, price range, brand, stock status, and rating - all optional. Building a JPQL string for every combination is tedious and error-prone. With Specifications, you create one small method per filter, and the admin's selections are combined dynamically. Each filter is independently testable, and the compiler validates field references when using metamodel classes.
Key Points
- The Criteria API builds queries programmatically, catching type errors at compile time.
- Spring Data Specifications wrap the Criteria API into composable, reusable filter objects.
- Predicates are combined with
and(),or(), andnot()for complex dynamic queries. - JPA metamodel classes (
Book_) provide full type safety by replacing string field references with typed attributes. - Specifications are ideal for dynamic search forms where filters are optional and combinable.