【问题标题】:Implement Search specification for pagination为分页实现搜索规范
【发布时间】:2021-06-05 18:01:06
【问题描述】:

我实现了这个页面请求:

@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);
}

问题是如何为这个Page 请求实现搜索功能? 我想将 typedateAdded 之类的参数发送到 GET 参数中并返回过滤结果?

【问题讨论】:

    标签: java spring spring-boot spring-data-jpa


    【解决方案1】:

    您可以通过多种方式实现所需的行为。

    让我们暂时忘记分页参数。

    首先,定义一个 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 中包含必要的字段pagesize,作为新字段。
    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)上使用缓存级别。

    尽管如此,这里还是有必要指出两件重要的事情:

    • 一,您可以安全地使用GETPOST HTTP 动词来处理您的搜索,所提供的解决方案对这两个动词都有效,只需进行少量修改。
    • 二,在您的实际用例中,使用一种或其他 HTTP 方法将高度依赖:
      • 例如,如果您要处理大量参数,则使用GET 动词时,URL 限制可能会成为问题。我自己也多次遇到过这个问题。
      • 如果不是这种情况,并且您的应用程序主要是分析性的,或者至少您正在处理不经常更改的静态信息或数据,请使用GET,HTTP 级缓存可以为您带来很大的好处。
      • 如果您的信息大部分是可操作的,并且有很多更改,您始终可以依靠服务器端缓存、数据库或服务层级别、Redis、Caffeine 等来提供缓存功能。这种方法通常会为您提供更精细的缓存逐出控制,以及通常的缓存管理。

    2 @blagerweij 在他的评论中也建议使用Slice。如果您不需要知道记录集的元素总数 - 例如,在滚动页面并触发获取固定数量的新记录集的典型用例中,将显示在页面中 - 使用 Slice 而不是 Page 可以为您提供巨大的性能优势。请考虑查看此SO question,例如。

    在一个典型的用例中,为了将SlicefindAll 一起使用,您的存储库不能扩展JpaRepository,因为它又扩展了PagingAndSortingRepository,并且该接口已经提供了您现在使用的方法findAll(Pageable pageable) .

    也许您可以改为使用 CrudRepository 并定义一个类似的方法:

    Slice<Product> findAll(Pageable pageable);
    

    但是,我不确定您是否可以将Slices 与Specifications 一起使用:请参阅this Github issue:恐怕它仍然是WIP。

    【讨论】:

    • 为了支持缓存,我仍然建议使用 GET 进行阅读。此外,无需使用 multipart/form-data(除非您正在上传二进制文件)。我建议使用 Slice 而不是 Page(因为 Page 需要对数据库进行额外的计数查询)。 Spring data JPA 已经内置了很多支持。
    • 非常感谢@blagerweij 的评论,非常感谢您的反馈。关于multipart/form-data,我当然想说标准 application/x-www-form-urlencoded,请见谅。无论如何,我从答案中删除了那部分。非常感谢您指出这些有趣的话题。请看我更新的答案。如果您认为合适,请随时提供任何进一步的信息。
    【解决方案2】:

    有两种方法可以实现:

    1. 示例。简单但只能用于简单搜索
        @GetMapping("products")
        public Page<ProductFullDTO> findProducts(Product example,
    Pageable pageable) {
       return productService.findProductsByExample(example, pageable);
    }
    

    为了能够通过示例进行搜索,您需要确保您的 ProductRepository 接口从 QueryByExampleExecutor 扩展:

    public interface ProductRepository 
      extends JpaRepository<Product, Long>, QueryByExampleExecutor<Product> {}
    

    在您的服务方法中,您可以简单地传递示例对象:

    public Page<ProductFullDTO> findProductsByExample(Product exampleProduct, Pageable pageable) {
        return productRepository.findByExample(Example.of(exampleProduct), pageable).map(ProductFullDTO::new);
    }
    

    更多信息请参见https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example.matchers

    1. 使用 JPA 规范。这在搜索参数更复杂的情况下很有用(例如,当您需要加入其他实体时)
        @GetMapping("products")
        public Page<ProductFullDTO> findProducts(@Valid ProductSearchParams params,
    Pageable pageable) {
       return productService.findProducts(params, pageable);
    }
    

    ProductSearchParams 类可能如下所示:

    public class ProductSearchParams {
       private String type;
       private Date dateAdded;
    }
    

    要使用搜索参数,我们需要一个规范。要使用它,您需要确保您的 ProductRepository 接口从 JpaSpecificationExecutor 扩展:

    public interface ProductRepository 
      extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {}
    

    由于 Specification 接口只有一个方法,我们可以在服务中使用 lambda:

    public Page<ProductFullDTO> findProducts(ProductSearchParams params, Pageable pageable) {
        Specification<Product> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (params.getType() != null) {
                predicates.add(cb.equal(root.get("type"), params.getType()));
            }
            if (params.getDateAdded() != null) {
                predicates.add(cb.greaterThan(root.get("dateAdded"), params.getDateAdded()));
            }
            return cb.and(predicates.toArray(new Predicate[predicates.size()]));
        };
        return productRepository.findAll(spec, pageable).map(ProductFullDTO::new);
    }
    

    使用规范更强大,但也需要更多的工作。有关使用规范的信息,请参阅https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications。使用规范还允许您编写非常优雅的表达式,例如customerRepository.findAll( isLongTermCustomer().or(hasSalesOfMoreThan(amount)));

    【讨论】:

      【解决方案3】:

      您想要实现Specification 接口,您可以将其传递到您的JPA 存储库。 toPredicate 方法的实现是您可以构建选择逻辑的地方。

      JpaSpecificationExecutor 中有一个重载:

      Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable); 
      

      有一个使用Specification/Repository 接口here 的好例子。

      【讨论】:

        猜你喜欢
        • 2020-02-16
        • 1970-01-01
        • 2012-06-08
        • 2015-08-18
        • 1970-01-01
        • 2018-05-20
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多