您可以通过多种方式实现所需的行为。
让我们暂时忘记分页参数。
首先,定义一个 POJO,它将作为搜索条件所需的不同字段粘合在一起。例如:
public class ProductFilter {
private String type;
private LocalDate dateAdded;
// Setters and getters omitted for brevity
}
此信息应该是您的Controller 搜索方法的入口点。
尽管@GetMapping 非常合适,但请考虑改用@PostMapping,主要是为了避免可能出现的URL length 问题1:
@PostMapping
public PageImpl<ProductFullDTO> list(ProductFilter filter) {
//...
}
或者在你的控制器中使用你的搜索条件作为 JSON 负载和@RequestBody:
@PostMapping
public PageImpl<ProductFullDTO> list(@RequestBody ProductFilter filter) {
//...
}
现在,如何处理Controller级别的分页相关信息?您也有多种选择。
- 您可以在
ProductFilter 中包含必要的字段page 和size,作为新字段。
public class ProductFilter {
private String type;
private LocalDate dateAdded;
private int page;
private int size;
// Setters and getters omitted for brevity
}
- 您可以创建一个通用 POJO 来处理分页字段并在您的过滤器中扩展它(也许您可以直接使用
PageRequest 本身,尽管我考虑一种更简单的方法来为这个功能创建自己的 POJO 以便维护独立于 Spring - 任何其他框架 - 尽可能):
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
}
- 您可以(这是我的首选)按原样维护您的过滤器,并修改 url 以包含分页信息。如果您使用的是
@RequestBody,这将特别有趣。
让我们考虑这种方法以继续对服务层进行必要的更改。请看相关代码,注意内联cmets:
@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;
}
您的page 方法可能如下所示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());
}
您可以根据需要在Product 上实施建议的Specification。一些一般提示:
- 始终在为任务定义的方法中的单独类中定义
Specification,这将允许您在代码的多个位置重用并有利于可测试性。
- 如果您愿意,为了提高代码的易读性,您可以在定义时使用 lambda。
- 要识别谓词构造中使用的不同字段,请始终使用元模型类而不是
Strings 作为字段名称。您可以使用Hibernate Metamodel generator 生成必要的工件。
- 在您的特定用例中,不要忘记包含必要的
sort 定义以提供一致的结果。
总而言之,buildProductFilterSpecification 可以如下所示:
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 正如@blagerweij 在他的评论中指出的那样,使用POST 而不是GET 将以某种方式阻止在HTTP(Web 服务器,Spring MVC)上使用缓存级别。
尽管如此,这里还是有必要指出两件重要的事情:
- 一,您可以安全地使用
GET 或POST HTTP 动词来处理您的搜索,所提供的解决方案对这两个动词都有效,只需进行少量修改。
- 二,在您的实际用例中,使用一种或其他 HTTP 方法将高度依赖:
- 例如,如果您要处理大量参数,则使用
GET 动词时,URL 限制可能会成为问题。我自己也多次遇到过这个问题。
- 如果不是这种情况,并且您的应用程序主要是分析性的,或者至少您正在处理不经常更改的静态信息或数据,请使用
GET,HTTP 级缓存可以为您带来很大的好处。
- 如果您的信息大部分是可操作的,并且有很多更改,您始终可以依靠服务器端缓存、数据库或服务层级别、Redis、Caffeine 等来提供缓存功能。这种方法通常会为您提供更精细的缓存逐出控制,以及通常的缓存管理。
2 @blagerweij 在他的评论中也建议使用Slice。如果您不需要知道记录集的元素总数 - 例如,在滚动页面并触发获取固定数量的新记录集的典型用例中,将显示在页面中 - 使用 Slice 而不是 Page 可以为您提供巨大的性能优势。请考虑查看此SO question,例如。
在一个典型的用例中,为了将Slice 与findAll 一起使用,您的存储库不能扩展JpaRepository,因为它又扩展了PagingAndSortingRepository,并且该接口已经提供了您现在使用的方法findAll(Pageable pageable) .
也许您可以改为使用 CrudRepository 并定义一个类似的方法:
Slice<Product> findAll(Pageable pageable);
但是,我不确定您是否可以将Slices 与Specifications 一起使用:请参阅this Github issue:恐怕它仍然是WIP。