spring-bootjpaspring-data-jpajpa-2.0jpa-2.1

Add WHERE IN clause to JPA Specification


I'm trying to implement search functionality limited by IN clause:

I want to implement search implementation with filter limitation:

    @GetMapping("find")
    public Page<MerchantUserDTO> getAllBySpecification(
            @And({
                @Spec(path = "name", spec = LikeIgnoreCase.class),
                @Spec(path = "login", spec = LikeIgnoreCase.class),
                @Spec(path = "email", spec = LikeIgnoreCase.class),
            }) Specification<Users> specification,
            @SortDefault(sort = "login", direction = Sort.Direction.DESC) Pageable pageable
    ) {        
        return merchantUserService.getAllBySpecification(specification, pageable)
                .map(g -> MerchantUserDTO.builder()                   
                        .id(g.getId())
                        .login(g.getLogin())                        
                        .build()
                );
    }

    @Override
    public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
        return dao.findAllByTypeIn(specification, pageable, "MerchantUser");
    }

Repository:

@Repository
public interface MerchantUserRepository extends JpaRepository<Users, Integer>, JpaSpecificationExecutor<Users> {

    Page<Users> findAllByTypeIn(Pageable page, String... types);

    Page<Users> findAllByTypeIn(Specification<Users> specification, Pageable pageable, String... types);
}

What is the proper way to extend the specification with IN clause?

specification.and(path.in(types)) path is a attribute but how to implement it properly?


Solution

  • Generally this can be achieved this way:

    1. Create specification implementation
        public class MerchantUserSpecification implements Specification<Users> {
        
            private final List<String> types;
        
            public MerchantUserSpecification(List<String> types) {
                this.types = types;
            }
        
            @Override
            public Predicate toPredicate(Root<Users> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                if (types != null && !types.isEmpty()) {
                    return root.get(Users_.type).in(types);
                } else {
                    // always-true predicate, means that no filtering would be applied
                    return cb.and(); 
                }
            }
    
    1. Use method Page findAll(@Nullable Specification spec, Pageable pageable); inherited from JpaSpecificationExecutor interface instead of using your custom findAllByTypeIn(Specification<Users> specification....)
        @Override
        public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
            // combine original specification (passed from outside) and filter-by-types specification
            Specification<Users> finalSpec = Specification.where(specification)
                   .and(new MerchantUserSpecification(Arrays.asList("MerchantUser")))
            return dao.findAll(finalSpec, pageable)
        }
    

    P.S.

    With Java 8+ and for simple cases (like yours) the code may be reduced even more. Instead of implementing Specification<T> in separate class you can just create a method

    private Specification<Users> typeIn(List<String> types) {
        return (root, query, cb) -> {
            if (types != null && !types.isEmpty()) {
               return root.get(Users_.type).in(types);
            } else {
               // always-true predicate, means that no filtering would be applied
               return cb.and(); 
            }
        }
    }
    
    @Override
    public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
        // combine original specification (passed from outside) and filter-by-types specification
        Specification<Users> finalSpec = Specification.where(specification)
                .and(typeIn(Arrays.asList("MerchantUser")))
        return dao.findAll(finalSpec, pageable)
    }
    

    UPDATE: Shortest way

    @Override
    public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
        // combine original specification (passed from outside) and filter-by-types specification
        Specification<Users> finalSpec = Specification.where(specification)
                .and((root, query, cb) -> root.get(Users_.type).in(Arrays.asList("MerchantUser"))
        return dao.findAll(finalSpec, pageable)
    }