no

Spring Data JPA: Creating Dynamic Search Filters with Specifications

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.

Related

spring-data 1302458893965737422

Post a Comment Default Comments

item