You can implement the desired behavior in several ways.
Let's forget about the paging parameters for a moment.
First, define a POJO that agglutinates the different fields required as search criteria. For example:
public class ProductFilter {
private String type;
private LocalDate dateAdded;
// Setters and getters omitted for brevity
}
This information should be the entry point to your Controller search method.
Please, although a @GetMapping is perfectly suitable, consider use a @PostMapping instead, mainly in order to avoid possible URL length problems 1:
@PostMapping
public PageImpl<ProductFullDTO> list(ProductFilter filter) {
//...
}
Or to consume your search criteria as JSON payload and @RequestBody in your controller:
@PostMapping
public PageImpl<ProductFullDTO> list(@RequestBody ProductFilter filter) {
//...
}
Now, how to deal with pagination related information at Controller level? You have several options as well.
- You can include the necessary fields,
page and size, as new fields in ProductFilter.
public class ProductFilter {
private String type;
private LocalDate dateAdded;
private int page;
private int size;
// Setters and getters omitted for brevity
}
- You can create a common POJO to deal with the pagination fields and extend it in your filters (maybe you can work directly with
PageRequest itself although I consider a simpler approach to create your own POJO for this functionality in order to maintain independence from Spring - an any other framework - as much as possible):
public class PagingForm {
private int page;
private int size;
//...
}
public class ProductFilter extend PagingForm {
private String type;
private LocalDate dateAdded;
// Setters and getters omitted for brevity
}
- You can (this is my preferred one) maintain your filter as is, and modify the url to include the paging information. This is especially interesting if you are using
@RequestBody.
Let's consider this approach to continue with the service layer necessary changes. Please, see the relevant code, pay attention to the inline comments:
@PostMapping
public PageImpl<ProductFullDTO> list(
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "10") int size,
@RequestBody ProductFilter filter
) {
PageRequest pageRequest = PageRequest.of(page, size);
// Include your filter information
PageImpl<ProductFullDTO> result = productRestService.page(filter, pageRequest);
return result;
}
Your page method can look like this 2:
public PageImpl<ProductFullDTO> page(final ProductFilter filter, final PageRequest pageRequest){
// As far as your repository extends JpaSpecificationExecutor, my advice
// will be to create a new Specification with the appropriate filter criteria
// In addition to precisely provide the applicable predicates,
// it will allow you to control a lot of more things, like fetch join
// entities if required, ...
Specification<Product> specification = buildProductFilterSpecification(filter);
// Use now the constructed specification to filter the actual results
Page<Product> pageResult = productService.findAll(specification, pageRequest);
List<ProductFullDTO> result = pageResult
.stream()
.map(productMapper::toFullDTO)
.collect(toList());
return new PageImpl<ProductFullDTO>(result, pageRequest, pageResult.getTotalElements());
}
You can implement the suggested Specification over Product as you need. Some general tips:
- Always define the
Specification in a separate class in a method defined for the task, it will allow you to reuse in several places of your code and favors testability.
- If you prefer, in order to improve the legibility of the code, you can use a lambda when defining it.
- To identify the different fields used in your predicate construction, prefer always the use of metamodel classes instead of
Strings for the field name. You can use the Hibernate Metamodel generator to generate the necessary artifacts.
- In your specific use case, do not forget to include the necessary
sort definition to provide consistent results.
In summary, the buildProductFilterSpecification can look like this:
public static Specification<Product> buildProductFilterSpecification(final ProjectFilter filter) {
return (root, query, cb) -> {
final List<Predicate> predicates = new ArrayList<>();
final String type = filter.getType();
if (StringUtils.isNotEmpty(type)) {
// Consider the use of like on in instead
predicates.add(cb.equal(root.get(Product_.type), cb.literal(type)));
}
// Instead of dateAdded, please, consider a date range, it is more useful
// Let's suppose that it is the case
final LocalDate dateAddedFrom = filter.getDateAddedFrom();
if (dateAddedFrom != null){
// Always, specially with dates, use cb.literal to avoid underlying problems
predicates.add(
cb.greaterThanOrEqualTo(root.get(Product_.dateAdded), cb.literal(dateAddedFrom))
);
}
final LocalDate dateAddedTo = filter.getDateAddedTo();
if (dateAddedTo != null){
predicates.add(
cb.lessThanOrEqualTo(root.get(Product_.dateAdded), cb.literal(dateAddedTo))
);
}
// Indicate your sort criteria
query.orderBy(cb.desc(root.get(Product_.dateAdded)));
final Predicate predicate = cb.and(predicates.toArray(new Predicate[predicates.size()]));
return predicate;
};
}
1 As @blagerweij pointed out in his comment, the use of POST instead of GET will prevent in a certain way the use of caching at HTTP (web server, Spring MVC) level.
Nevertheless, it is necessary to indicate two important things here:
- one, you can safely use a
GET or POST HTTP verb to handle your search, the solution provided will be valid with both verbs with minimal modifications.
- two, the use of one or other HTTP method will be highly dependent in your actual use case:
- If, for example, you are dealing with a lot of parameters, the URL limit can be a problem if you use
GET verb. I faced the problem myself several times.
- If that is not the case and your application is mostly analytical, or at least you are working with static information or data that does not change often, use
GET, HTTP level cache can provide you great benefits.
- If your information is mostly operational, with numerous changes, you can always rely on server side cache, at database or service layer level, with Redis, Caffeine, etcetera, to provide caching functionally. This approach will usually provide you a more granular control about cache eviction and, in general, cache management.
2 @blagerweij suggested in his comment the use of Slice as well. If you do not need to know the total number of elements of your record set - think, for example, in the typical use case in which you scroll a page and it fires the fetch of a new record set, in a fixed amount, that will be shown in the page - the use of Slice instead of Page can provide you great performance benefits. Please, consider review this SO question, for instance.
In a typical use case, in order to use Slice with findAll your repository cannot extend JpaRepository because in turn it extends PagingAndSortingRepository and that interface already provides the method you are using now, findAll(Pageable pageable).
Probably you can expend CrudRepository instead and the define a method similar to:
Slice<Product> findAll(Pageable pageable);
But, I am not sure if you can use Slices with Specifications: please, see this Github issue: I am afraid that it is still a WIP.