ImplementSearchspecificationforpagination
I implemented this Page request:
@GetMapping
public PageImpl<ProductFullDTO> list(@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "10") int size) {
PageRequest pageRequest = PageRequest.of(page, size);
PageImpl<ProductFullDTO> result = productRestService.page(pageRequest);
return result;
}
public PageImpl<ProductFullDTO> page(PageRequest pageRequest){
Page<Product> pageResult = productService.findAll(pageRequest);
List<ProductFullDTO> result = pageResult
.stream()
.map(productMapper::toFullDTO)
.collect(toList());
return new PageImpl<ProductFullDTO>(result, pageRequest, pageResult.getTotalElements());
}
public Page<Product> findAll(PageRequest pageRequest) {
return this.dao.findAll(pageRequest);
}
@Repository
public interface ProductRepository extends JpaRepository<Product, Integer>, JpaSpecificationExecutor<Product> {
Page<Product> findAllByTypeIn(Pageable page, String... types);
Page<Product> findAll(Pageable page);
}
The question is how to implement search functionality for this Page
request?
I would like to send params like type
and dateAdded
into GET params and return filtered result?
回答
您可以通过多种方式实现所需的行为。
让我们暂时忘记分页参数。
首先,定义一个 POJO,将所需的不同字段作为搜索条件进行凝集。例如:
public class ProductFilter {
private String type;
private LocalDate dateAdded;
// Setters and getters omitted for brevity
}
此信息应该是您的Controller
搜索方法的入口点。
请尽管 a@GetMapping
非常合适,但请考虑使用 a@PostMapping
代替,主要是为了避免可能出现的URL 长度问题1:
@PostMapping
public PageImpl<ProductFullDTO> list(ProductFilter filter) {
//...
}
或者将您的搜索条件作为 JSON 负载并@RequestBody
在您的控制器中使用:
@PostMapping
public PageImpl<ProductFullDTO> list(@RequestBody ProductFilter filter) {
//...
}
现在,如何在Controller
级别处理分页相关信息?您也有多种选择。
- 您可以包括必要的字段,
page
并且size
,在新的领域ProductFilter
。
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
.
让我们考虑这种方法来继续对服务层进行必要的更改。请看相关代码,注意内联注释:
@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());
}
您可以执行建议Specification
过Product
,因为你需要。一些一般提示:
- 始终
Specification
在为任务定义的方法中的单独类中定义 ,这将允许您在代码的多个位置重用并有利于可测试性。 - 如果您愿意,为了提高代码的易读性,您可以在定义时使用 lambda。
- 要识别谓词构造中使用的不同字段,请始终使用元模型类而不是
String
s 作为字段名称。您可以使用Hibernate Metamodel 生成器来生成必要的工件。 - 在您的特定用例中,不要忘记包含必要的
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)级别使用缓存。
尽管如此,这里有必要指出两件重要的事情:
- 一,您可以安全地使用 a
GET
或POST
HTTP 动词来处理您的搜索,所提供的解决方案将对这两个动词都有效,只需进行最少的修改。 - 二、使用一种或其他 HTTP 方法将高度依赖于您的实际用例:
- 例如,如果您正在处理大量参数,那么如果您使用
GET
动词,则 URL 限制可能是一个问题。我自己多次遇到这个问题。 - 如果情况并非如此,并且您的应用程序主要是分析性的,或者至少您正在处理不经常更改的静态信息或数据,那么使用
GET
HTTP 级缓存可以为您提供很大的好处。 - 如果您的信息主要是可操作的,并且有很多变化,您可以始终依赖服务器端缓存,在数据库或服务层级别,使用 Redis、Caffeine 等,提供功能性缓存。这种方法通常会为您提供关于缓存逐出和一般缓存管理的更细粒度的控制。
- 例如,如果您正在处理大量参数,那么如果您使用
2 @blagerweij 在他的评论中也建议使用Slice
。如果您不需要知道记录集的元素总数 - 例如,在滚动页面并以固定数量触发新记录集的获取的典型用例中,将显示在页面中 - 使用Slice
而不是Page
可以为您提供巨大的性能优势。例如,请考虑查看这个SO 问题。
在典型的用例中,为了Slice
与findAll
您的存储库一起使用,无法扩展,JpaRepository
因为它反过来扩展PagingAndSortingRepository
并且该接口已经提供了您现在正在使用的方法,findAll(Pageable pageable)
.
可能您可以CrudRepository
改为expend并定义类似于以下内容的方法:
Slice<Product> findAll(Pageable pageable);
但是,我不确定您是否可以将Slice
s 与 s 一起使用Specification
:请参阅此 Github 问题:恐怕它仍然是 WIP。