0

I have an endpoint to get all Posts, I also have multiple @RequestParams used to filter and search for values etc.

The issue I'm having is that when trying to filter based on specific @RequestParams, I would need to have multiple checks to see whether that specific parameter is passed when calling the endpoint, so in my Controller I have something like this. The parameters are optional, I also have parameters for Pagination etc, but I left it out below.

I have these criteria:

@RequestParam(required=false) List<String> brand - Used to filter by multiple brands
@RequestParam(required=false) String province - Used to filter by province
@RequestParam(required=false) String city - Used to filter by city
// Using these 2 for getting Posts within a certain price range
@RequestParam(defaultValue = "0", required = false) String minValue - Used to filter by min price
@RequestParam(defaultValue = "5000000", required = false) String maxValue - Used to filter by max price

I also have this in my Controller when checking which of my service methods to call based on the parameters passed.

        if(query != null) {
            pageTuts = postService.findAllPosts(query, pagingSort);
        } else if(brand != null) {
            pageTuts = postService.findAllByBrandIn(brand, pagingSort);
        } else if(minValue != null && maxValue != null) {
            pageTuts = postService.findAllPostsByPriceBetween(minValue, maxValue, pagingSort);
        } else if(brand != null & minValue != null & maxValue != null) {
            pageTuts = postService.findAllPostsByPriceBetween(minValue, maxValue, pagingSort);
        } else {
            // if no parameters are passed in req, just get all the Posts available
            pageTuts = postService.findAllPosts(pagingSort);
        }
    // I would need more checks to handle all parameters


The issue is that I'm struggling to find out, if I need this condition for each and every possible parameter, which will be a lot of checks and Repository/Service methods based on that parameter.

For example in my Repository I have abstract methods like these:

    Page<Post> findAllByProvince(String province, Pageable pageable);
    Page<Post> findAllByCity(String city, Pageable pageable);
    Page<Post> findAllByProvinceAndCity(String province, String city, Pageable pageable);
    Page<Post> findAllByBrandInAndProvince(List<String> brand, String province, Pageable pageable);

And I'd need much more so I could handle the other potential values, ie. findAllByPriceBetween(), findAllByCityAndPriceBetween(), findAllByProvinceAndPriceBetween()...

So I'd like some suggestions on how to handle this?.


Edit

Managed to get it working by overriding the toPredicate method as shown by @M. Deinum with some small tweaks according to my use case.


    @Override
    public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) {
        List<Predicate> predicates = new ArrayList<>();

        // min/max is never not set as they have default values
        predicates.add(builder.between(root.get("price"), params.getMinValue(), params.getMaxValue()));

        if (params.getProvince() != null) {
            predicates.add(builder.equal(root.get("province"), params.getProvince()));
        }

        if (params.getCity() != null) {
            predicates.add(builder.equal(root.get("city"), params.getCity()));
        }


        
        if (!CollectionUtils.isEmpty(params.getBrand())) {
        Expression<String> userExpression = root.get("brand");
        Predicate p = userExpression.in(params.getBrand());
            predicates.add(p);
        }
        return builder.and(predicates.toArray(new Predicate[0]));
    }
1
  • Bind to an object which holds all the values. Then use predicates to dynamically build a query based on which are set.
    – M. Deinum
    Commented Sep 8, 2022 at 7:52

1 Answer 1

4
  1. Create an object to hold your variables instead of individual elements.
  2. Move the logic to your service and pass the object and pageable to the service
  3. Ditch those findAll methods from your repository and add the JpaSpecificationExecutor in your extends clause.
  4. In the service create Predicate and use the JpaSpecificationExecutor.findAll to return what you want.
public class PostSearchParameters {
 
  private String province;
  private String city;
  private List<String> brand;
  private int minValue = 0;
  private int maxValue = 500000;

  //getters/setters or when on java17+ use a record instead of class
}

Predicate

public class PostSearchParametersSpecification implements Specification {

  private final PostSearchParameters params;

  PostSearchParametersPredicate(PostSearchParameters params) {
    this.params=params;
  }

  @Override
  public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {

    List<Predicate> predicates = new ArrayList<>();
    // min/max is never not set as they have default values
    predicates.add(builder.between(root.get("price", params.getMinValue(), params.getMaxValue());

    if (params.getProvince() != null) {
      predicates.add(builder.equal(root.get("province"), params.getProvince());
    }

    if (params.getCity() != null) {
      predicates.add(builder.equal(root.get("city"), params.getCity());
    }

    if (!CollectionUtils.isEmpty(params.getBrand()) {
      predicates.add(builder.in(root.get("brand")).values( params.getBrand());
    }

    return builder.and(predicates.toArray(new Predicate[0]));
  }
}

Repository

public interface PostRepository extends JpaRepository<Post, Long>, JpaSpecificationExecutor<Post> {}

Service method

public Page<Post> searchPosts(PostSearchParameters params, Pageable pageSort) {
  PostSearchParametersSpecification specification =
    new PostSearchParametersSpecification(params)
  return repository.findAll(specification, pageSort);
}

Now you can query on all available parameters, adding one is extending/modifying the predicate and you are good to go.

See also the Spring Data JPA Reference guide on Specifications

5
  • Thanks, I'm trying to understand it and will let you know how I get along, also, should the PostSearchParametersPredicate class not implement Specification instead of Predicate? Since the toPredicate() method is a method within the Specification Interface. Commented Sep 8, 2022 at 11:03
  • My bad, that is what you get from typing it from the top of your head. Changed the implements.
    – M. Deinum
    Commented Sep 8, 2022 at 11:21
  • Thank you, managed to get it working with some few tweaks since I was having issues with the predicates.add(builder.in(root.get("brand", params.getBrand());, I used Expression<String> s = root.get("brand") and also created a separate Predicate for the IN clause, then I just added that Predicate to my List of predicates. Thanks once again, it is much cleaner and clutter free this way. Commented Sep 8, 2022 at 17:08
  • Edited my post with what I have now, please let me know if that's fine Commented Sep 8, 2022 at 17:11
  • If it works for you then why should I need to review your code :).
    – M. Deinum
    Commented Sep 9, 2022 at 8:15

Not the answer you're looking for? Browse other questions tagged or ask your own question.