1. 概述

本教程将演示如何使用Spring Data JPAList<Object>转换为Page<Object>。在Spring Data JPA应用中,从数据库分页获取数据是常见操作。但有时我们需要将实体列表转换为Page对象用于分页接口,例如从外部API获取数据或内存处理数据。

我们将通过一个简单示例展示数据流和转换过程。系统分为RestControllerService[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-jpaspring-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实例。它将封装pageContentpageRequest以及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=1size=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>之间的转换,利用了PagePageablePageImpl等API。最后简要分析了使用Page<Object>而非List<Object>的优势。

总结:在REST接口中将List<Object>转换为Page<Object>,为处理大型数据集和实现API分页提供了更高效、标准化和灵活的方式。

完整源代码可在GitHub获取。


原始标题:Converting List to Page Using Spring Data JPA