Specifications
Criteria, the Spring Data Way
Spring Data JPA wraps the Criteria API in Specifications: small reusable filter pieces that you combine with and / or. Extend JpaSpecificationExecutor and you get findAll(spec), findAll(spec, pageable), count(spec) and more.
public interface ProductRepository extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> { }
public final class ProductSpecs {
private ProductSpecs() {}
public static Specification<Product> inCategory(String name) {
return (root, query, cb) -> cb.equal(root.get("category").get("name"), name);
}
public static Specification<Product> priceBetween(BigDecimal min, BigDecimal max) {
return (root, query, cb) -> cb.between(root.get("price"), min, max);
}
public static Specification<Product> inStock() {
return (root, query, cb) -> cb.greaterThan(root.get("stock"), 0);
}
public static Specification<Product> nameContains(String text) {
return (root, query, cb) -> cb.like(cb.lower(root.get("name")), "%" + text.toLowerCase() + "%");
}
}Combining Specifications for a Search Endpoint
@GetMapping("/api/products/search")
public Page<ProductCardDto> search(@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal min,
@RequestParam(required = false) BigDecimal max,
@RequestParam(defaultValue = "false") boolean inStock,
@RequestParam(required = false) String q,
Pageable pageable) {
List<Specification<Product>> specs = new ArrayList<>();
if (category != null) specs.add(ProductSpecs.inCategory(category));
if (min != null && max != null) specs.add(ProductSpecs.priceBetween(min, max));
if (inStock) specs.add(ProductSpecs.inStock());
if (q != null && !q.isBlank()) specs.add(ProductSpecs.nameContains(q));
return productRepository.findAll(Specification.allOf(specs), pageable)
.map(ProductCardDto::from);
}Request, SQL and response
curl "localhost:8080/api/products/search?category=Electronics&min=500&max=3000&inStock=true&page=0&size=2&sort=price"Example output
DEBUG org.hibernate.SQL : select p1_0.id,p1_0.category_id,p1_0.description,p1_0.name,p1_0.price,p1_0.stock from products p1_0 join categories c1_0 on c1_0.id=p1_0.category_id where c1_0.name=? and p1_0.price between ? and ? and p1_0.stock>? order by p1_0.price limit ?,?
DEBUG org.hibernate.SQL : select count(p1_0.id) from products p1_0 join categories c1_0 on c1_0.id=p1_0.category_id where c1_0.name=? and p1_0.price between ? and ? and p1_0.stock>?
HTTP/1.1 200 OK
{"content":[{"id":19,"name":"boAt Rockerz 255","price":899.00},{"id":7,"name":"Boat Airdopes 141","price":1099.00}],
"page":{"size":2,"number":0,"totalElements":14,"totalPages":7}}The Idea in Plain Java
Example
A Specification is a composable predicate that also knows how to render itself as SQL. This toy version uses java.util.function.Predicate to filter an in-memory list and builds the WHERE clause from the same pieces.
import java.util.*;
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
List<Product> catalog = List.of(
new Product("Boat Airdopes 141", "Electronics", 1099, 40),
new Product("Mi Power Bank", "Electronics", 1499, 0),
new Product("Cotton Kurta", "Fashion", 799, 12),
new Product("boAt Rockerz 255", "Electronics", 899, 8),
new Product("Gaming Mouse", "Electronics", 3499, 5));
// filters from the request: category=Electronics, max=2000, inStock=true
List<Spec> specs = new ArrayList<>();
specs.add(Spec.inCategory("Electronics"));
specs.add(Spec.maxPrice(2000));
specs.add(Spec.inStock());
Spec all = specs.stream().reduce(Spec::and).orElse(Spec.ALL);
System.out.println("where " + all.sql());
catalog.stream().filter(all.test())
.sorted(Comparator.comparingInt(Product::price))
.forEach(p -> System.out.println(" " + p.name() + " - Rs." + p.price()));
}
}
record Product(String name, String category, int price, int stock) {}
record Spec(String sql, Predicate<Product> test) {
static final Spec ALL = new Spec("1=1", p -> true);
static Spec inCategory(String c) { return new Spec("c.name='" + c + "'", p -> p.category().equals(c)); }
static Spec maxPrice(int max) { return new Spec("p.price<=" + max, p -> p.price() <= max); }
static Spec inStock() { return new Spec("p.stock>0", p -> p.stock() > 0); }
Spec and(Spec o) { return new Spec(sql + " and " + o.sql, test.and(o.test)); }
}Output
where c.name='Electronics' and p.price<=2000 and p.stock>0 boAt Rockerz 255 - Rs.899 Boat Airdopes 141 - Rs.1099
Tips
Tip: Specification.allOf(...) with an empty list matches everything, so "no filters" needs no special case. Use Specification.anyOf(...) for OR.
Note: A Specification that joins a collection (root.join("items")) can duplicate rows; call query.distinct(true) inside it. For fetch joins inside specifications, remember that the same lambda is also used for the count query, where fetch joins are not allowed: check Long.class != query.getResultType() before calling root.fetch(...).