Search forms often have conditions that may or may not be filled in. Sometimes only the name is specified and the category is left blank; other times every field including price range is filled. Trying to handle such variable conditions with static JPQL via @Query quickly becomes a dead end.
On the other hand, stacking if statements in the service layer tends to look like this:
if (name != null && category != null) {
return repo.findByNameAndCategory(name, category);
} else if (name != null) {
return repo.findByName(name);
} else if (category != null) {
return repo.findByCategory(category);
} else {
return repo.findAll();
}
With just three conditions, you already have eight branches. Let’s solve this with the Specification pattern.
Extend Your Repository with JpaSpecificationExecutor
All you need to do is add JpaSpecificationExecutor to your existing JpaRepository.
public interface ProductRepository
extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> {
}
This gives you access to findAll(Specification<T> spec) and findAll(Specification<T> spec, Pageable pageable). The entity is assumed to look like this:
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
private BigDecimal price;
private String category;
// getters/setters omitted
}
Understand the Three Elements of the Criteria API
Implementing toPredicate in a Specification involves three objects:
- Root<T>: The entity in the FROM clause. Use
root.get("name")to reference a column. - CriteriaBuilder: A factory for creating WHERE conditions (Predicates).
- Predicate: The actual condition expression, built with
cb.equal(...),cb.like(...), etc.
You write your conditions inside toPredicate using these three objects.
Writing a Single-Condition Specification
Implementing with a lambda expression is the simplest approach.
public class ProductSpecifications {
public static Specification<Product> nameContains(String name) {
return (root, query, cb) ->
cb.like(root.get("name"), "%" + name + "%");
}
public static Specification<Product> categoryEquals(String category) {
return (root, query, cb) ->
cb.equal(root.get("category"), category);
}
}
Grouping these as static methods in a utility class keeps the call sites clean.
Safely Skipping Null and Empty Conditions
When a search condition is not provided, return cb.conjunction(). This is an “always true” Predicate, which effectively skips that WHERE condition.
public static Specification<Product> nameContains(String name) {
return (root, query, cb) -> {
if (name == null || name.isEmpty()) {
return cb.conjunction();
}
return cb.like(root.get("name"), "%" + name + "%");
};
}
Passing a null value directly to cb.like(...) will cause a NullPointerException, so make it a habit to add this check at the top of each method.
Combining Multiple Conditions with AND/OR
Multiple Specifications can be chained using Specification.where().and().or().
Specification<Product> spec = Specification
.where(ProductSpecifications.nameContains(form.getName()))
.and(ProductSpecifications.categoryEquals(form.getCategory()))
.and(ProductSpecifications.priceBetween(form.getMinPrice(), form.getMaxPrice()));
where() just creates a starting point, so there is no problem if the first Specification returns null. Use .or(spec) when you want OR-based joining.
A Utility Class to Build Specifications from a Form Bean
In real-world applications, a common pattern is to receive a Bean that holds search conditions and convert it into a Specification.
@Getter @Setter
public class ProductSearchForm {
private String name;
private String category;
private BigDecimal minPrice;
private BigDecimal maxPrice;
}
public class ProductSpecifications {
public static Specification<Product> from(ProductSearchForm form) {
return Specification
.where(nameContains(form.getName()))
.and(categoryEquals(form.getCategory()))
.and(priceBetween(form.getMinPrice(), form.getMaxPrice()));
}
private static Specification<Product> priceBetween(BigDecimal min, BigDecimal max) {
return (root, query, cb) -> {
if (min == null && max == null) return cb.conjunction();
if (min == null) return cb.lessThanOrEqualTo(root.<BigDecimal>get("price"), max);
if (max == null) return cb.greaterThanOrEqualTo(root.<BigDecimal>get("price"), min);
return cb.between(root.<BigDecimal>get("price"), min, max);
};
}
// nameContains, categoryEquals are as shown above
}
In the service layer, all you need to do is call from(form), and the chain of if statements disappears.
Combining with Pagination
Using findAll(spec, pageable) lets you combine a Specification with pagination as-is.
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public Page<Product> search(ProductSearchForm form, Pageable pageable) {
return productRepository.findAll(ProductSpecifications.from(form), pageable);
}
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
@GetMapping("/search")
public Page<Product> search(ProductSearchForm form, Pageable pageable) {
return productService.search(form, pageable);
}
}
Spring automatically binds Pageable from query parameters like ?page=0&size=10&sort=name,asc. For a broader look at pagination, see “Spring BootのREST APIでページネーションを実装する方法”.
When to Use QueryDSL Instead
Specifications work without any additional libraries, making them a solid fit for simple search forms. If your joins become complex or you want type-safe code with IDE autocompletion, QueryDSL has the advantage — though it comes with the overhead of setting up code generation. If your goal is simply to clean up dynamic queries, starting with Specifications is the pragmatic choice.
Summary
Adopting the Specification pattern frees you from the if-statement hell in your service layer.
- Enabled simply by extending
JpaSpecificationExecutor - Safely skip null conditions with
cb.conjunction() - Express multiple conditions by chaining
where().and().or() - Grouping Specification creation into a utility class that accepts a form Bean keeps call sites simple
findAll(spec, pageable)works seamlessly with pagination
For the basics of query methods, see “Spring Data JPAのクエリメソッド、どう書けばいいか迷ったことありませんか?”. For entity relationship design, also refer to “Spring Boot JPAエンティティのリレーション設計”.