1. 概述
Spring MVC 和 Spring Data 都能很好地簡化各自的應用開發。但如果我們將它們結合起來呢?在本教程中,我們將探討 Spring Data 的 Web 支持 以及 其 resolvers 能夠減少冗餘代碼並使我們的控制器更具表現力。
在學習過程中,我們還會窺探一下 Querydsl 以及它與 Spring Data 的集成情況。
2. 背景介紹
Spring Data 的 Web 支持是建立在標準 Spring MVC 平台之上的,一組與 Web 相關的特性,旨在為控制器層添加額外的功能。
Spring Data Web 支持的功能建立在多個 解析器 類之上。解析器簡化了控制器方法與 Spring Data 存儲庫交互的實現,並以額外的功能來豐富它們。
這些功能包括 從存儲庫層檢索領域對象,無需顯式調用存儲庫實現,以及 構建可以作為數據片段發送給客户端,支持分頁和排序的控制器響應。
此外,針對帶有多個請求參數的控制器方法請求可以內部解析為 Querydsl 查詢。
3. 一個 Spring Boot 項目演示
為了理解如何使用 Spring Data Web 支持來改進我們的控制器功能,讓我們創建一個基本的 Spring Boot 項目。
我們的演示項目的 Maven 依賴關係相當標準,僅有一些例外情況,我們稍後會討論它們:
<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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
在這種情況下,我們包含了 spring-boot-starter-web,因為我們會用它來創建 RESTful 控制器,spring-boot-starter-jpa 來實現持久層,以及 spring-boot-starter-test 用於測試控制器 API。
由於我們會使用 H2 作為底層數據庫,我們還包含了 com.h2database。
請記住,spring-boot-starter-web 默認情況下通過啓用 Spring Data Web 支持。因此,我們不需要創建任何額外的 @Configuration 類來在我們的應用程序中使其工作。
相反,對於非 Spring Boot 項目,我們需要定義一個 @Configuration 類並用 @EnableWebMvc 和 @EnableSpringDataWebSupport 標註它。
3.1. 領域類
現在,讓我們將一個簡單的 User JPA 實體類添加到項目中,以便我們有一個可用的領域模型來玩:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private final String name;
// standard constructor / getters / toString
}
3.2. 持久性層
為了使代碼儘可能簡單,我們應用程序的演示功能將僅限於從 H2 內存數據庫檢索一些 User 實體。
Spring Boot 使創建提供最小 CRUD 功能的倉庫實現變得容易。因此,讓我們定義一個與 User JPA 實體一起工作的簡單倉庫接口:
@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {}
在 UserRepository 接口的定義中沒有任何複雜之處,除了它繼承了 PagingAndSortingRepository。
這會通知 Spring MVC 啓用數據庫記錄的自動分頁和排序功能。
3.3. 控制器層
現在,我們需要實現至少一個基本的 RESTful 控制器,作為客户端和倉庫層之間的中間層。
因此,讓我們創建一個控制器類,該類接受一個 UserRepository 實例,並添加一個用於查找 User 實體的方法 id:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User findUserById(@PathVariable("id") User user) {
return user;
}
}
3.4. 運行應用程序
最後,讓我們定義應用程序的主類並填充 H2 數據庫中的一些 User 實體:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
CommandLineRunner initialize(UserRepository userRepository) {
return args -> {
Stream.of("John", "Robert", "Nataly", "Helen", "Mary").forEach(name -> {
User user = new User(name);
userRepository.save(user);
});
userRepository.findAll().forEach(System.out::println);
};
}
}
現在,讓我們運行應用程序。正如我們所期望的,啓動時,我們看到保存的 User 實體列表打印到控制枱:
User{id=1, name=John}
User{id=2, name=Robert}
User{id=3, name=Nataly}
User{id=4, name=Helen}
User{id=5, name=Mary}
4. The DomainClassConverter 類
目前,UserController 類只實現了findUserById()方法。
乍一看,該方法實現看起來相當簡單。但實際上,它封裝了大量的Spring Data Web支持功能。
由於該方法接受一個User實例作為參數,我們可能會認為需要顯式地在請求中傳遞領域對象。但我們不需要。
Spring MVC使用DomainClassConverter類,將id路徑變量轉換為領域類的id類型,並用於從存儲層檢索匹配的領域對象。
無需進行任何額外的查找。
例如,一個向http://localhost:8080/users/1端點的GET HTTP請求將返回以下結果:
{
"id":1,
"name":"John"
}
因此,我們可以創建一個集成測試並檢查findUserById()方法的行為:
@Test
public void whenGetRequestToUsersEndPointWithIdPathVariable_thenCorrectResponse() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/users/{id}", "1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"));
}
或者,我們可以使用REST API測試工具,如Postman,來測試該方法。
DomainClassConverter的優點在於,我們不需要在控制器方法中顯式地調用存儲層實現。
只需指定id路徑變量,以及一個可解析的領域類實例,我們就可以自動觸發領域對象的查找。
5. The PageableHandlerMethodArgumentResolver Class
Spring MVC supports the use of Pageable types in controllers and repositories.
Simply put, a Pageable instance is an object that holds paging information. Therefore, when we pass a Pageable argument to a controller method, Spring MVC uses the PageableHandlerMethodArgumentResolver class to resolve the Pageable instance into a PageRequest object, which is a simple Pageable implementation.
Pageable as a Controller Method Parameter
To understand how the PageableHandlerMethodArgumentResolver class works, let’s add a new method to the UserController class:
@GetMapping("/users")
public Page<User> findAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
In contrast to the findUserById() method, here we need to call the repository implementation to fetch all the User JPA entities persisted in the database.
Since the method takes a Pageable instance, it returns a subset of the entire set of entities, stored in a Page<User> object.
A Page objectis a sublist of a list of objects that exposes several methods we can use for retrieving information about the paged results, including the total number of result pages, and the number of the page that we’re retrieving.
By default, Spring MVC uses the PageableHandlerMethodArgumentResolver class to construct a PageRequest object, with the following request parameters:
- page: the index of page that we want to retrieve – the parameter is zero-indexed and its default value is 0
- size: the number of pages that we want to retrieve – the default value is 20
- sort: one or more properties that we can use for sorting the results, using the following format: property1,property2(,asc|desc) – for instance, ?sort=name&sort=email,asc
For example, a GET request to the http://localhost:8080/usersss endpoint will return the following output:
{
"content":[
{
"id":1,
"name":"John"
},
{
"id":2,
"name":"Robert"
},
{
"id":3,
"name":"Nataly"
},
{
"id":4,
"name":"Helen"
},
{
"id":5,
"name":"Mary"
}],
"pageable":{
"sort":{
"sorted":false,
"unsorted":true,
"empty":true
},
"pageSize":5,
"pageNumber":0,
"offset":0,
"unpaged":false,
"paged":true
},
"last":true,
"totalElements":5,
"totalPages":1,
"numberOfElements":5,
"first":true,
"size":5,
"number":0,
"sort":{
"sorted":false,
"unsorted":true,
"empty":true
},
"empty":false
}
As we can see, the response includes the first, pageSize, totalElements, and totalPages JSON elements. This is really useful since a front-end can use these elements for easily creating a paging mechanism.
In addition, we can use an integration test to check the findAllUsers() method:
@Test
public void whenGetRequestToUsersEndPoint_thenCorrectResponse() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/users")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$['pageable']['paged']").value("true"));
}
5.2. Customizing the Paging Parameters
In many cases, we’ll want to customize the paging parameters. The simplest way to accomplish this is by using the @PageableDefault annotation:
@GetMapping("/users")
public Page<User> findAllUsers(@PageableDefault(value = 2, page = 0) Pageable pageable) {
return userRepository.findAll(pageable);
}
Alternatively, we can use PageRequest’s of() static factory method to create a custom PageRequest object and pass it to the repository method:
@GetMapping("/users")
public Page<User> findAllUsers() {
Pageable pageable = PageRequest.of(0, 5);
return userRepository.findAll(pageable);
}
The first parameter is the zero-based page index, while the second one is the size of the page that we want to retrieve.
In the example above, we created a PageRequest object of User entities, starting with the first page (0), with the page having 5 entries.
Additionally, we can build a PageRequest object using the page and size request parameters:
@GetMapping("/users")
public Page<User> findAllUsers(@RequestParam("page") int page,
@RequestParam("size") int size, Pageable pageable) {
return userRepository.findAll(pageable);
}
Using this implementation, a GET request to the http://localhost:8080/users?page=0&size=2 endpoint will return the first page of User objects, and the size of the result page will be 2:
{
"content": [
{
"id": 1,
"name": "John"
},
{
"id": 2,
"name": "Robert"
}
],
// continues with pageable metadata
}
6. SortHandlerMethodArgumentResolver 類別
分頁是有效地管理大量數據庫記錄的一種常用方法。但單獨使用它如果不能以某種特定方式對記錄進行排序,那也是無用的。
為此,Spring MVC 提供
6.1. 使用 sort 控制器方法參數
為了瞭解
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@RequestParam("sort") String sort, Pageable pageable) {
return userRepository.findAll(pageable);
}
在這種情況下,
因此,向
{
"content": [
{
"id": 4,
"name": "Helen"
},
{
"id": 1,
"name": "John"
},
{
"id": 5,
"name": "Mary"
},
{
"id": 3,
"name": "Nataly"
},
{
"id": 2,
"name": "Robert"
}
],
// continues with pageable metadata
}
6.2. 使用 Sort.by() 靜態工廠方法
或者,我們可以使用
在這種情況下,我們將僅按
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName() {
Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
return userRepository.findAll(pageable);
}
當然,我們可以使用多個屬性,只要它們在域類中聲明即可。
6.3. 使用 @SortDefault 註解
同樣,我們可以使用
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@SortDefault(sort = "name",
direction = Sort.Direction.ASC) Pageable pageable) {
return userRepository.findAll(pageable);
}
最後,讓我們創建一個集成測試來檢查該方法的功能:
@Test
public void whenGetRequestToSorteredUsersEndPoint_thenCorrectResponse() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/sortedusers")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$['sort']['sorted']").value("true"));
}
7. Querydsl Web Support
如我們在引言中所提到的,Spring Data 的 Web 支持允許我們使用控制器方法中的請求參數來構建 Querydsl 的 Predicate 類型,並構建 Querydsl 查詢。
為了保持簡單,我們將僅查看 Spring MVC 如何將請求參數轉換為 Querydsl 的 Predicate 類型,而該類型又被傳遞給 QuerydslPredicateExecutor。
要實現這一點,首先我們需要添加 querydsl-apt 和 querydsl-jpa Maven 依賴項到 pom.xml 文件中:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.1.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.1.0</version>
<classifier>jakarta</classifier>
</dependency>
接下來,我們需要重構我們的 UserRepository 接口,該接口也必須擴展 QuerydslPredicateExecutor 接口:
@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long>,
QuerydslPredicateExecutor<User> {
}
最後,讓我們為 UserController 類添加以下方法:
@GetMapping("/filteredusers")
public Iterable<User> getUsersByQuerydslPredicate(@QuerydslPredicate(root = User.class)
Predicate predicate) {
return userRepository.findAll(predicate);
}
雖然方法實現看起來相當簡單,但它實際上暴露了大量功能在下面。
假設我們想從數據庫中檢索所有名稱匹配的 User 實體。我們可以通過簡單地調用該方法並指定 URL 中的 name 請求參數來實現:
http://localhost:8080/filteredusers?name=John
正如我們所預期的,請求將返回以下結果:
[
{
"id": 1,
"name": "John"
}
]
正如我們之前所做的那樣,我們可以使用集成測試來檢查 getUsersByQuerydslPredicate() 方法:
@Test
public void whenGetRequestToFilteredUsersEndPoint_thenCorrectResponse() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/filteredusers")
.param("name", "John")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("John"));
}
這只是 Querydsl Web 支持的工作方式的一個基本示例。但它並沒有揭示其全部力量。
現在,假設我們想檢索一個 User 實體,該實體的 id 與給定 id 匹配。在這種情況下,我們只需將 id 請求參數傳遞到 URL 中:
http://localhost:8080/filteredusers?id=2
在這種情況下,我們會得到以下結果:
[
{
"id": 2,
"name": "Robert"
}
]
可以清楚地看到,Querydsl Web 支持是一個非常強大的功能,我們可以使用它來檢索滿足給定條件的數據庫記錄。
在所有情況下,整個過程都簡化為調用一個控制器方法,並使用不同的請求參數。
8. 結論
在本教程中,我們深入研究了 Spring Web 支持的關鍵組件,並學習瞭如何在演示 Spring Boot 項目中使用它們。