Spring Data JPA: Creating Dynamic Search Filters with Specifications
https://www.czetsuyatech.com/2025/03/spring-data-jpa-creating-dynamic-search-filters-with-specification.html.html
1. Introduction
In this article, I will share an implementation that utilizes a common base class to streamline the process of implementing search functionality in Spring Data JPA using Specifications. By creating a reusable base class, you can efficiently manage and extend your search logic across multiple entities. This approach reduces redundancy and promotes cleaner, more maintainable code while allowing for dynamic query creation based on varying search criteria. Whether you're dealing with simple or complex filtering requirements, this solution will simplify your data querying layer in Spring applications.
2. Supported Operations
The following operations are supported:
- greater than
- less than
- greater than or equal
- less than or equal
- equal
- isnull
- like
These operation will be listed as enum for easier reference.
3. Base Model
We will use a user entity to demonstrate the filter.
package com.czetsuyatech.persistence.entities; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; @Table(name = "user_profile") @Entity @Data public class UserEntity extends BaseEntity { @Column(name = "first_name") private String firstName; @Column(name = "last_name") private String lastName; @Column(name = "role") private String role; @Column(name = "age") private int age; }
4. Class / Flow Diagram
As we can see above we will need a query builder that accepts UserSpecification and UserFields to generate the user entity specification that we need to feed to the UserRepository extending SliceJpaRepository, which is implemented by SimpleSliceJpaRepositoryImpl.
5. Repositories and Services
5.1 Anatomy of the user repository
@Repository public interface UserRepository extends SliceJpaRepository<UserEntity, Long> { }
@NoRepositoryBean public interface SliceJpaRepository<ENTITY, ID extends Serializable> extends JpaRepository<ENTITY, ID>, JpaSpecificationExecutor<ENTITY>, JpaSpecificationExecutorWithProjection<ENTITY> { Slice<ENTITY> findAllSlice(@Nullable Specification<ENTITY> specification, Pageable pageable); }
5.2 Query Builder
The query builder accepts the available filterable parameters of the user entity and the specification which define how we are going to implement the filter using criteria builder.
public enum UserSearchFields { FIRSTNAME { @Override public String toString() { return "firstName"; } }, LASTNAME { @Override public String toString() { return "lastName"; } }, ROLE { @Override public String toString() { return "role"; } }, AGE { @Override public String toString() { return "age"; } } }
@AllArgsConstructor public class UserSpecification implements Specification<UserEntity> { private transient SearchCriteria criteria; @Override public Predicate toPredicate(Root<UserEntity> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { UserSearchFields key = UserSearchFields.valueOf(StringUtils.upperCase(criteria.getKey())); RelationalOperators operator = RelationalOperators.getOperator(criteria.getOperation()); Object value = criteria.getValue(); Expression<?> expression = root.<String>get(key.toString()); Predicate predicate = null; switch (operator) { case LIKE -> predicate = buildLike(key, value, expression, criteriaBuilder); case EQUAL -> predicate = buildEqual(key, value, expression, criteriaBuilder); case GREATER, GREATER_THAN_EQUAL -> predicate = buildGreater(key, value, expression, criteriaBuilder); case LESS, LESS_THAN_EQUAL -> predicate = buildLess(key, value, expression, criteriaBuilder); default -> throw new IllegalStateException("Unexpected value: " + operator); } return predicate; } private Predicate buildLess(UserSearchFields key, Object value, Expression<?> expression, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.lessThanOrEqualTo((Expression<Integer>) expression, Integer.parseInt(value.toString())); } private Predicate buildGreater(UserSearchFields key, Object value, Expression<?> expression, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.greaterThanOrEqualTo((Expression<Integer>) expression, Integer.parseInt(value.toString())); } private Predicate buildEqual(UserSearchFields key, Object value, Expression<?> expression, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.equal(expression, value); } private Predicate buildLike(UserSearchFields key, Object value, Expression<?> expression, CriteriaBuilder criteriaBuilder) { return criteriaBuilder.like((Expression<String>) expression, value.toString() + SpecificationConstant.LIKE_WILDCARD); } }
5.3 The user service that invokes the repository.
@Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; @Transactional(readOnly = true) @Override public Slice<UserDTO> findUsers(String searchParams, Pageable pageable) { try { Specification<UserEntity> specResult = QueryBuilder.build(searchParams, UserSpecification.class, UserSearchFields.class); return userRepository.findAllSlice(specResult, pageable).map(userMapper::userToUserDTO); } catch (Exception e) { e.printStackTrace(); } return new SliceImpl<>(Collections.emptyList()); } }
6. Controller
And this is how we are going to call the service from the controller.
@RequestMapping("/users") @RestController @RequiredArgsConstructor @Slf4j public class UserController { private final UserService userService; @GetMapping public ResponseEntity<Slice<UserDTO>> findUsers(@RequestParam(name = "search", required = false) String search, @PageableDefault Pageable pageable) { log.debug("Search: " + search); return ResponseEntity.ok(userService.findUsers(search, pageable)); } }
7. Postman Tests
8. Development and Support
I'm available for contracting services and support.
Post a Comment