概述
本文将探讨分页在获取信息中的重要性,并比较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
,它提供了使用分页和排序抽象获取实体的方法。这些方法接受Pageable
和Sort
对象,用于配置返回的Page
信息。Page
对象包含totalElements
和totalPages
属性,这些是在内部执行额外查询后填充的。这些信息可用于请求后续页的信息。
相比之下,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分页的独特性质,并实现了一个返回产品列表并带有分页的端点。