1. 概述
本教程将演示如何使用Spring Data JPA将List<Object>
转换为Page<Object>
。在Spring Data JPA应用中,从数据库分页获取数据是常见操作。但有时我们需要将实体列表转换为Page
对象用于分页接口,例如从外部API获取数据或内存处理数据。
我们将通过一个简单示例展示数据流和转换过程。系统分为RestController
、Service
和[Repository](/spring-data-repositories)
三层,演示如何将从数据库获取的List<Object>
转换为更小的分页数据。最后编写测试验证分页效果。
2. Spring Data JPA的核心分页抽象
快速了解Spring Data JPA提供的核心分页抽象组件。
2.1. Page
Page
是Spring Data提供的关键接口,用于表示分页格式的结果集。我们可以用Page
对象向用户展示指定数量的记录,并提供导航到其他页面的链接。
Page
封装了页面内容以及分页元数据,包括页码、页面大小、是否存在下一页/上一页、剩余元素数量、总页数和总元素数。
2.2. Pageable
Pageable
是分页信息的抽象接口,其实现类是PageRequest
。它表示分页元数据,如当前页码、每页元素数和排序条件。这是Spring Data JPA中用于指定查询分页信息的接口,在我们的场景中,它将分页信息与内容打包,实现从List<Object>
创建Page<Object>
。
2.3. PageImpl
PageImpl
类提供了Page
接口的便捷实现,用于表示查询结果的分页数据,包含分页元数据。它通常与Spring Data的Repository接口和分页机制配合使用,以分页方式检索和操作数据。
理解这些组件后,我们开始搭建示例。
3. 示例搭建
考虑一个客户信息微服务,提供基于请求参数的分页客户数据REST接口。首先在POM中添加依赖:spring-boot-starter-data-jpa
、spring-boot-starter-web
和测试用的spring-boot-starter-test
:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
接下来设置REST Controller。
3.1. 客户控制器
添加处理请求参数的方法,驱动Service层逻辑:
@GetMapping("/api/customers")
public ResponseEntity<Page<Customer>> getCustomers(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) {
Page<Customer> customerPage = customerService.getCustomers(page, size);
HttpHeaders headers = new HttpHeaders();
headers.add("X-Page-Number", String.valueOf(customerPage.getNumber()));
headers.add("X-Page-Size", String.valueOf(customerPage.getSize()));
return ResponseEntity.ok()
.headers(headers)
.body(customerPage);
}
注意:getCustomers
方法期望返回ResponseEntity<Page<Customer>>
类型。
3.2. 客户服务
设置Service类,与Repository交互,将数据转换为Page
并返回给Controller:
public Page<Customer> getCustomers(int page, int size) {
List<Customer> allCustomers = customerRepository.findAll();
//... 将List<Customer>转换为Page<Customer>的逻辑
//... 返回Page<Customer>
}
这里省略了细节,重点在于Service调用JPA Repository获取List<Customer>
格式的客户数据。接下来我们详细说明如何使用JPA API将列表转换为Page<Customer>
。
4. 将List<Customer>
转换为Page<Customer>
现在详细说明CustomerService
如何将Repository返回的List<Customer>
转换为Page
对象。从数据库获取所有客户列表后,我们使用PageRequest
工厂方法创建Pageable
对象:
private Pageable createPageRequestUsing(int page, int size) {
return PageRequest.of(page, size);
}
注意这些page和size参数是从CustomerRestController
传递到CustomerService
的请求参数。
然后我们将大客户列表分割为子列表。需要计算起始和结束索引,基于此创建子列表。这些索引可通过Pageable
对象的getOffset()
和getPageSize()
方法计算:
int start = (int) pageRequest.getOffset();
获取结束索引:
int end = Math.min((start + pageRequest.getPageSize()), allCustomers.size());
该子列表将作为Page
对象的内容:
List<Customer> pageContent = allCustomers.subList(start, end);
最后创建PageImpl
实例。它将封装pageContent
、pageRequest
以及List<Customer>
的总大小:
new PageImpl<>(pageContent, pageRequest, allCustomers.size());
整合所有代码:
public Page<Customer> getCustomers(int page, int size) {
Pageable pageRequest = createPageRequestUsing(page, size);
List<Customer> allCustomers = customerRepository.findAll();
int start = (int) pageRequest.getOffset();
int end = Math.min((start + pageRequest.getPageSize()), allCustomers.size());
List<Customer> pageContent = allCustomers.subList(start, end);
return new PageImpl<>(pageContent, pageRequest, allCustomers.size());
}
5. 将Page<Customer>
转换为List<Customer>
处理大型结果集时,分页是重要功能。Spring Data提供了便捷的分页支持。要在查询方法中添加分页,需将方法签名改为接受Pageable
参数并返回Page<T>
而非List<T>
。
返回的Page
对象表示结果的一部分,包含元素列表、总元素数和总页数等信息。
例如,获取指定页的客户列表:
public List<Customer> getCustomerListFromPage(int page, int size) {
Pageable pageRequest = createPageRequestUsing(page, size);
Page<Customer> allCustomers = customerRepository.findAll(pageRequest);
return allCustomers.hasContent() ? allCustomers.getContent() : Collections.emptyList();
}
我们使用相同的createPageRequestUsing()
工厂方法创建Pageable
对象,然后调用getContent()
方法获取客户列表。
注意:调用getContent()
前使用hasContent()
检查页面是否有内容。
6. 测试服务层
编写测试验证List<Customer>
是否正确分割为Page<Customer>
,以及页大小和页码是否正确。模拟customerRepository.findAll()
返回大小为20的客户列表。
在设置中,当调用findAll()
时提供该列表:
@BeforeEach
void setup() {
when(customerRepository.findAll()).thenReturn(ALL_CUSTOMERS);
}
使用参数化测试验证内容、内容大小、总元素数和总页数:
@ParameterizedTest
@MethodSource("testIO")
void givenAListOfCustomers_whenGetCustomers_thenReturnsDesiredDataAlongWithPagingInformation(int page, int size, List<String> expectedNames, long expectedTotalElements, long expectedTotalPages) {
Page<Customer> customers = customerService.getCustomers(page, size);
List<String> names = customers.getContent()
.stream()
.map(Customer::getName)
.collect(Collectors.toList());
assertEquals(expectedNames.size(), names.size());
assertEquals(expectedNames, names);
assertEquals(expectedTotalElements, customers.getTotalElements());
assertEquals(expectedTotalPages, customers.getTotalPages());
}
参数化测试的输入输出数据:
private static Collection<Object[]> testIO() {
return Arrays.asList(
new Object[][] {
{ 0, 5, PAGE_1_CONTENTS, 20L, 4L },
{ 1, 5, PAGE_2_CONTENTS, 20L, 4L },
{ 2, 5, PAGE_3_CONTENTS, 20L, 4L },
{ 3, 5, PAGE_4_CONTENTS, 20L, 4L },
{ 4, 5, EMPTY_PAGE, 20L, 4L } }
);
}
每个测试使用不同页码(0,1,2,3,4)运行服务方法,每页预期5个元素。由于原始列表总大小为20,预期总页数为4。
接下来测试将Page<Customer>
转换为List<Customer>
。首先测试Page
对象非空的情况:
@Test
void givenAPageOfCustomers_whenGetCustomerList_thenReturnsList() {
Page<Customer> pagedResponse = new PageImpl<Customer>(ALL_CUSTOMERS.subList(0, 5));
when(customerRepository.findAll(any(Pageable.class))).thenReturn(pagedResponse);
List<Customer> customers = customerService.getCustomerListFromPage(0, 5);
List<String> customerNames = customers.stream()
.map(Customer::getName)
.collect(Collectors.toList());
assertEquals(PAGE_1_CONTENTS.size(), customers.size());
assertEquals(PAGE_1_CONTENTS, customerNames);
}
这里模拟customerRepository.findAll(pageRequest)
返回包含ALL_CUSTOMERS
子集的Page
对象,返回的客户列表与Page
对象中的列表相同。
测试customerRepository.findAll(pageRequest)
返回空页的情况:
@Test
void givenAnEmptyPageOfCustomers_whenGetCustomerList_thenReturnsEmptyList() {
Page<Customer> emptyPage = Page.empty();
when(customerRepository.findAll(any(Pageable.class))).thenReturn(emptyPage);
List<Customer> customers = customerService.getCustomerListFromPage(0, 5);
assertThat(customers).isEmpty();
}
返回列表为空,符合预期。
7. 测试控制器层
测试Controller确保返回JSON格式的ResponseEntity<Page<Customer>>
。使用MockMVC
向GET接口发送请求,验证分页响应:
@Test
void givenTotalCustomers20_whenGetRequestWithPageAndSize_thenPagedReponseIsReturnedFromDesiredPageAndSize() throws Exception {
MvcResult result = mockMvc.perform(get("/api/customers?page=1&size=5"))
.andExpect(status().isOk())
.andReturn();
MockHttpServletResponse response = result.getResponse();
JSONObject jsonObject = new JSONObject(response.getContentAsString());
assertThat(jsonObject.get("totalPages")).isEqualTo(4);
assertThat(jsonObject.get("totalElements")).isEqualTo(20);
assertThat(jsonObject.get("number")).isEqualTo(1);
assertThat(jsonObject.get("size")).isEqualTo(5);
assertThat(jsonObject.get("content")).isNotNull();
}
使用MockMvc
模拟HTTP GET请求到/api/customers
接口,提供查询参数page=1
和size=5
,预期成功响应且响应体包含分页元数据和内容。
最后分析将List<Customer>
转换为Page<Customer>
对API设计和使用的优势。
8. 使用Page<Customer>
而非List<Customer>
的优势
在API响应中选择返回Page<Customer>
而非整个List<Customer>
,根据使用场景有以下优势:
✅ 优化网络流量和处理效率:当底层数据源返回大型客户列表时,转换为Page<Customer>
允许客户端仅请求特定页的结果而非整个列表,简化客户端处理并减少网络负载。
✅ 标准化响应格式:返回Page<Customer>
提供客户端易于理解和消费的标准化响应格式,包含请求页的客户列表以及总页数、每页项目数等元数据。
✅ API设计灵活性:将对象列表转换为页面增强了API设计灵活性,例如允许客户端按不同字段排序结果、根据条件过滤结果,或返回每个客户的字段子集。
9. 总结
本教程使用Spring Data JPA处理List<Object>
和Page<Object>
之间的转换,利用了Page
、Pageable
和PageImpl
等API。最后简要分析了使用Page<Object>
而非List<Object>
的优势。
总结:在REST接口中将List<Object>
转换为Page<Object>
,为处理大型数据集和实现API分页提供了更高效、标准化和灵活的方式。
完整源代码可在GitHub获取。