概述

本文将探讨分页在获取信息中的重要性,并比较Spring Data和Spring Data Reactive的分页实现方式,并通过一个示例演示如何实现分页。

分页的重要性

当处理返回大量资源的端点时,分页是一个关键概念。它通过将数据分解成更小、可管理的部分(称为“页面”),实现了高效的数据检索和展示。

设想一个显示产品详情的用户界面,可能需要显示10到10,000条记录。如果前端设计为从后端一次性加载并展示整个目录,这将消耗额外的后台资源,导致用户等待时间显著增加。

实施分页系统可以显著提升用户体验。与其一次性获取所有记录,不如先获取一部分,然后根据需要提供加载下一页的选项。

通过分页,后端可以首先返回一个包含少量子集(如10条记录)的初始响应,然后使用偏移量或下一页链接来获取后续页面。这种方法将数据获取和展示的负载分散到多个页面上,从而改善整体应用体验。

Spring Data与Spring Data Reactive中的分页

Spring Data 是Spring框架生态系统的一部分,旨在简化Java应用程序中的数据访问。Spring Data提供了一套通用抽象和功能,通过减少样板代码和推广最佳实践,简化了开发流程。

Spring Data分页示例所述,可以使用PageRequest对象(接受页面号、大小和排序参数)来配置和请求不同的页面。Spring Data提供了PagingAndSortingRepository,它提供了使用分页和排序抽象获取实体的方法。这些方法接受PageableSort对象,用于配置返回的Page信息。Page对象包含totalElementstotalPages属性,这些是在内部执行额外查询后填充的。这些信息可用于请求后续页的信息。

相比之下,Spring Data Reactive对分页的支持并不完整。其原因在于Spring Reactive支持异步非阻塞,必须等待(或阻塞)直到返回特定页面大小的所有数据,这效率不高。然而,Spring Data Reactive仍然支持Pageable。我们可以使用PageRequest对象来配置获取特定数据块,并添加明确的查询以获取记录总数。

使用Spring Data时,我们得到的是Flux响应流,而非Page,其中包含页面记录的元数据。

基本应用

4.1. 在Spring WebFlux和Spring Data Reactive中实现分页

本文将使用一个简单的Spring R2DBC应用,通过GET /products端点提供带有分页的产品信息。

考虑一个简单的Product模型:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Table
public class Product {

    @Id
    @Getter
    private UUID id;

    @NotNull
    @Size(max = 255, message = "The property 'name' must be less than or equal to 255 characters.")
    private String name;

    @NotNull
    private double price;
}

我们可以通过传递一个Pageable对象从Product Repository获取产品列表,该对象包含页面号(Page)和大小(Size)等配置:

@Repository
public interface ProductRepository extends ReactiveSortingRepository<Product, UUID> {
    Flux<Product> findAllBy(Pageable pageable);   
}

这个查询响应的结果集作为Flux而不是Page,因此需要单独查询总数来填充Page响应。

让我们添加一个控制器,使用PageRequest对象,并运行一个额外的查询来获取记录总数。这是因为我们的repository不会返回Page信息,而是返回Flux<Product>

@GetMapping("/products")
public Mono<Page<Product>> findAllProducts(Pageable pageable) {
    return this.productRepository.findAllBy(pageable)
      .collectList()
      .zipWith(this.productRepository.count())
      .map(p -> new PageImpl<>(p.getT1(), pageable, p.getT2()));
}

最后,我们必须将查询结果集和原本接收到的Pageable对象发送给PageImpl。这个类有辅助方法来计算Page信息,包括用于获取下一页记录的页面元数据。

现在,当我们尝试访问端点时,我们应该收到带有页面元数据的产品列表:

{
  "content": [
    {
      "id": "cdc0c4e6-d4f6-406d-980c-b8c1f5d6d106",
      "name": "product_A",
      "price": 1
    },
    {
      "id": "699bc017-33e8-4feb-aee0-813b044db9fa",
      "name": "product_B",
      "price": 2
    },
    {
      "id": "8b8530dc-892b-475d-bcc0-ec46ba8767bc",
      "name": "product_C",
      "price": 3
    },
    {
      "id": "7a74499f-dafc-43fa-81e0-f4988af28c3e",
      "name": "product_D",
      "price": 4
    }
  ],
  "pageable": {
    "sort": {
      "sorted": false,
      "unsorted": true,
      "empty": true
    },
    "pageNumber": 0,
    "pageSize": 20,
    "offset": 0,
    "paged": true,
    "unpaged": false
  },
  "last": true,
  "totalElements": 4,
  "totalPages": 1,
  "first": true,
  "numberOfElements": 4,
  "size": 20,
  "number": 0,
  "sort": {
    "sorted": false,
    "unsorted": true,
    "empty": true
  },
  "empty": false
}

就像Spring Data一样,我们通过某些查询参数跳转到不同的页面,并通过扩展WebMvcConfigurationSupport,配置默认属性。

让我们将默认页大小改为100,同时设置默认页为0,通过重写addArgumentResolvers方法实现:

@Configuration
public class CustomWebMvcConfigurationSupport extends WebMvcConfigurationSupport {

    @Bean
    public PageRequest defaultPageRequest() {
        return PageRequest.of(0, 100);
    }

    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        SortHandlerMethodArgumentResolver argumentResolver = new SortHandlerMethodArgumentResolver();
        argumentResolver.setSortParameter("sort");
        PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(argumentResolver);
        resolver.setFallbackPageable(defaultPageRequest());
        resolver.setPageParameterName("page");
        resolver.setSizeParameterName("size");
        argumentResolvers.add(resolver);
    }
}

现在,我们可以从第0页开始,每页最多100条记录进行请求:

$ curl --location 'http://localhost:8080/products?page=0&size=50&sort=price,DESC'

如果没有指定页码和大小参数,默认页索引为0,每页100条记录。但请求设置了页大小为50:

{
  "content": [
    ....
  ],
  "pageable": {
    "sort": {
      "sorted": false,
      "unsorted": true,
      "empty": true
    },
    "pageNumber": 0,
    "pageSize": 50,
    "offset": 0,
    "paged": true,
    "unpaged": false
  },
  "last": true,
  "totalElements": 4,
  "totalPages": 1,
  "first": true,
  "numberOfElements": 4,
  "size": 50,
  "number": 0,
  "sort": {
    "sorted": false,
    "unsorted": true,
    "empty": true
  },
  "empty": false
}

总结

在这篇文章中,我们理解了Spring Data Reactive分页的独特性质,并实现了一个返回产品列表并带有分页的端点。