SkillByAIOpen interactive version →

Lesson 17 / 25

Criteria API and Specifications

Build dynamic, type-safe queries with the Criteria API and Spring Data Specifications.

Queries built in code

Search screens with optional filters (name contains, price range, category, in stock) lead to many possible query shapes. Concatenating JPQL strings for each combination is error-prone. The Criteria API builds queries programmatically: a CriteriaBuilder creates predicates, a CriteriaQuery defines selection, where, orderBy and joins, and the JPA metamodel (generated classes such as Product_.price) makes attribute references type-safe, so renaming a field breaks compilation rather than queries at run time. Criteria code is verbose, so Spring Data JPA Specifications wrap it: each filter becomes a small Specification<T> that you combine with and/or, and repositories extending JpaSpecificationExecutor run them with paging and sorting. Alternatives include Querydsl and Blaze-Persistence for more fluent dynamic queries, and jOOQ when you prefer type-safe SQL. Use static JPQL when a query's shape is fixed; use Criteria or Specifications when it is genuinely dynamic.

Composable Specifications for a product search

Only the filters provided by the user are applied.

public final class ProductSpecs {
    public static Specification<Product> titleContains(String text) {
        return (root, query, cb) -> text == null ? null
            : cb.like(cb.lower(root.get("title")), "%" + text.toLowerCase() + "%");
    }
    public static Specification<Product> priceBetween(BigDecimal min, BigDecimal max) {
        return (root, query, cb) -> {
            if (min == null && max == null) return null;
            if (min == null) return cb.le(root.get("price"), max);
            if (max == null) return cb.ge(root.get("price"), min);
            return cb.between(root.get("price"), min, max);
        };
    }
    public static Specification<Product> inCategory(String category) {
        return (root, query, cb) -> category == null ? null
            : cb.equal(root.join("category").get("slug"), category);
    }
}

public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> { }

Page<Product> page = productRepository.findAll(
    Specification.allOf(ProductSpecs.titleContains(q),
                        ProductSpecs.priceBetween(min, max),
                        ProductSpecs.inCategory(category)),
    PageRequest.of(0, 20, Sort.by("price").ascending()));

LEGO bricks for queries

Each Specification is a LEGO brick representing one filter. A search screen snaps together only the bricks the user selected, instead of keeping a separate pre-built model for every possible combination.

Quick check: When are the Criteria API or Spring Data Specifications most useful?

  • For queries whose shape never changes
  • For dynamic queries where filters are optional and combined at run time
  • For database migrations
  • For configuring connection pools
Answer

For dynamic queries where filters are optional and combined at run time — They build query predicates programmatically, avoiding string concatenation for dynamic filters.