使用 Spring Data JPA 將列表轉換為頁面
一、概述
在本教程中,我們將了解如何使用 Spring Data JPA 將List<Object>
轉換為Page<Object>
。在 Spring Data JPA 應用程序中,以可分頁的方式從數據庫中檢索數據是很常見的。但是,在某些情況下,我們可能需要將實體列表轉換為Page
對像以用於可分頁端點。例如,我們可能希望從外部 API 檢索數據或在內存中處理數據。
我們將設置一個簡單的示例來幫助我們可視化數據流和轉換。我們將把我們的系統分為RestController
、 Service
和Repository
層,看看我們如何使用 Spring Data 提供的分頁抽象將從List<Object>
中檢索到的大量數據轉換為更小、更有條理的頁面JPA。最後,我們將編寫一些測試來觀察分頁的效果。
2. Spring Data JPA 中的關鍵分頁抽象
讓我們簡要了解一下 Spring Data JPA 提供的用於生成分頁數據的關鍵抽象。
2.1. Page
Page
是Spring Data為方便分頁提供的關鍵接口之一。它提供了一種以分頁格式表示和管理從數據庫查詢返回的大型結果集的方法。
然後我們可以使用Page
對象向用戶顯示所需數量的記錄和導航到後續頁面的鏈接。
Page
封裝了頁面內容等詳細信息,以及涉及分頁詳細信息的元數據,例如頁碼和頁面大小、是否有下一頁或上一頁、剩餘多少元素以及頁面和元素的總數。
2.2. Pageable
Pageable
是分頁信息的抽象接口。實現此接口的具體類是PageRequest
。它表示分頁元數據,例如當前頁碼、每頁元素數和排序標準.
它是 Spring Data JPA 中的一個接口,提供了一種方便的方法來指定查詢的分頁信息,或者在我們的例子中,將分頁信息與內容捆綁在一起以從 List
2.3. PageImpl
最後,還有PageImpl
類,它提供了Page
接口的便捷實現,可用於表示查詢結果的頁面,包括分頁元數據。它通常與 Spring Data 的存儲庫接口和分頁機制結合使用,以可分頁的方式檢索和操作數據。
現在我們已經對涉及的組件有了基本的了解,讓我們建立一個簡單的例子。
3. 示例設置
讓我們考慮一個客戶信息微服務的簡單示例,其中包含一個可以根據請求參數獲取分頁客戶數據的 REST 端點。我們將首先在 POM 中設置所需的依賴項。所需的依賴項是[spring-boot-starter-data-jpa](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa)
和[spring-boot-starter-web](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web)
,我們還將添加[spring-boot-starter-test](https://mvnrepository.com/artifact/org.springframework.boot/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>
</dependendencies>
下一個。讓我們設置 REST 控制器。
3.1.客戶控制器
首先,讓我們添加帶有請求參數的適當方法,這些參數將驅動我們在服務層中的邏輯:
@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
方法需要一個Page<Customer>
類型的ResponseEntity
。
3.2.客戶服務
接下來,讓我們設置將與存儲庫交互的服務類,將數據轉換為所需的Page
,並將其返回給Controller
:
public Page<Customer> getCustomers(int page, int size) {
List<Customer> allCustomers = customerRepository.findAll();
//... logic to convert the List<Customer> to Page<Customer>
//... return Page<Customer>
}
在這裡,我們省略了細節,只關注服務調用 JPA 存儲庫並獲取潛在的大型Customer
數據集as List<Customer>
事實。接下來,讓我們詳細了解如何使用 JPA 提供的 API 將此列表轉換為Page<Customer>.
4. 將List<Customer>
轉換為Page<Customer>
現在,讓我們詳細說明CustomerService
如何將從CustomerRepository
收到的List<Customer>
轉換為Page
對象。本質上,在從數據庫中檢索到所有客戶的列表後,我們希望使用PageRequest
工廠方法創建一個Pageable
對象:
private Pageable createPageRequestUsing(int page, int size) {
return PageRequest.of(page, size);
}
請注意,這些頁面和大小參數是作為請求參數從我們的CustomerRestController
傳遞給CustomerService
參數。
然後,我們會將大Customer
列表拆分為一個子列表。我們需要知道開始和結束索引,我們可以根據它們創建子列表。這可以使用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. 測試服務
讓我們編寫一個快速測試,看看List<Customer>
是否拆分為Page<Customer>
以及頁面大小和頁碼是否正確。我們將模擬 c ustomerRepository.findAll()
方法以返回大小為 20 的Customer
列表。
在設置中,我們只需在調用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 個元素。我們預計總頁數為 4,因為原始列表的總大小為 20。最後,預計每頁包含 5 個元素。
6. 測試控制器
最後,我們還要測試 Controller 以確保我們將ResponseEntity<Page<Customer>>
返回為 JSON .
我們將使用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
實例來模擬對/api/customers
端點的 HTTP GET 請求。我們提供查詢參數page=1
和size=5.
然後我們期待一個成功的響應和一個包含頁面元數據和內容的正文。
最後,讓我們快速了解一下將List<Customer>
轉換為Page<Customer>
對 API 設計和使用有何好處。
7. 使用Page<Customer>
優於List<Customer>
的好處
在我們的示例中,選擇在整個List<Customer>
上返回Page<Customer>
作為 API 響應可能有一些好處,具體取決於用例。好處之一是優化網絡流量和處理。基本上,如果底層數據源返回大量客戶列表,將其轉換為Page<Customer>
允許客戶端僅請求特定頁面的結果,而不是整個列表。這可以簡化客戶端的處理並減少網絡負載。
此外,通過**返回Page<Customer>
,API 提供了一種客戶可以輕鬆理解和使用的標準化響應格式**。 Page<Customer>
對象包含所請求頁面的客戶列表和元數據,例如頁面總數和每頁的項目數。
最後,將對象列表轉換為頁面為 API 設計提供了靈活性。例如,API 可以允許客戶端按不同字段對結果進行排序。我們還可以根據條件過濾結果,甚至為每個客戶返回一個字段子集。
八、結論
在本教程中,我們使用 Spring Data JPA 將List<Object>
轉換為Page<Object>
。我們使用了 Spring Data JPA 提供的 API,包括Page
、 Pageable,
和PageImpl
類。最後,我們簡要研究了使用Page<Object>
而不是List<Object>
的一些好處。
總之,在 REST 端點中將List<Object>
轉換為Page<Object>
提供了一種更高效、標準化和靈活的方式來處理大型數據集並在 API 中實現分頁。
與往常一樣,可以在 GitHub 上找到本文的完整源代碼。