Spring Data Web Support – Spring数据网络支持

最后修改: 2019年 4月 19日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Spring MVC and Spring Data each do a great job simplifying application development in their own right. But, what if we put them together?

Spring MVCSpring Data各自在简化应用开发方面做得很好。但是,如果我们将它们放在一起呢?

In this tutorial, we’ll take a look at Spring Data’s web support and how its resolvers can reduce boilerplate and make our controllers more expressive.

在本教程中,我们将了解Spring Data的网络支持以及resolvers如何减少模板并使我们的控制器更具表现力。

Along the way, we’ll peek at Querydsl and what its integration with Spring Data looks like.

在此过程中,我们将偷看Querydsl以及它与Spring Data的集成情况。

2. A Bit of Background

2.一点背景

Spring Data’s web support is a set of web-related features implemented on top of the standard Spring MVC platform, aimed at adding extra functionality to the controller layer.

Spring Data的网络支持是在标准的Spring MVC平台之上实现的一组网络相关的功能,旨在为控制器层增加额外的功能

Spring Data web support’s functionality is built around several resolver classes. Resolvers streamline the implementation of controller methods that interoperate with Spring Data repositories and also enrich them with additional features.

Spring Data Web 支持的功能是围绕几个resolver类构建的。解析器简化了与Spring Data资源库互操作的控制器方法的实现,并且还通过额外的功能来丰富它们。

These features include fetching domain objects from the repository layer, without having to explicitly call the repository implementations, and constructing controller responses that can be sent to clients as segments of data that support pagination and sorting.

这些功能包括从资源库层获取域对象无需明确调用资源库实现,以及构建控制器响应,可作为支持分页和排序的数据段发送给客户端。

Also, requests to controller methods that take one or more request parameters can be internally resolved to Querydsl queries.

此外,对采取一个或多个请求参数的控制器方法的请求可以在内部解析为Querydsl>查询。

3. A Demo Spring Boot Project

3.一个演示的Spring Boot项目

To understand how we can use Spring Data web support to improve our controllers’ functionality, let’s create a basic Spring Boot project.

为了了解我们如何使用Spring Data网络支持来改善控制器的功能,我们来创建一个基本的Spring Boot项目。

Our demo project’s Maven dependencies are fairly standard, with a few exceptions that we’ll discuss later on:

我们的演示项目的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>

In this case, we included spring-boot-starter-web, as we’ll use it for creating a RESTful controller, spring-boot-starter-jpa for implementing the persistence layer, and spring-boot-starter-test for testing the controller API.

在这种情况下,我们包括spring-boot-starter-web,因为我们将用它来创建一个RESTful控制器,spring-boot-starter-jpaem>用于实现持久层,spring-boot-starter-testem>用于测试控制器 API。

Since we’ll use H2 as the underlying database, we included com.h2database as well.

由于我们将使用H2作为底层数据库,我们也包括com.h2database

Let’s keep in mind that spring-boot-starter-web enables Spring Data web support by default. Hence, we don’t need to create any additional @Configuration classes to get it working within our application.

让我们记住,spring-boot-starter-web默认启用Spring Data网络支持。因此,我们不需要创建任何额外的@Configuration类来让它在我们的应用程序中工作。

Conversely, for non-Spring Boot projects, we’d need to define a @Configuration class and annotate it with the @EnableWebMvc and @EnableSpringDataWebSupport annotations.

相反,对于非Spring Boot项目,我们需要定义一个@Configuration类,并用@EnableWebMvc@EnableSpringDataWebSupport注解来注释它。

3.1. The Domain Class

3.1.领域类

Now, let’s add a simple User JPA entity class to the project, so we can have a working domain model to play with:

现在,让我们在项目中添加一个简单的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. The Repository Layer

3.2.存储库层

To keep the code simple, the functionality of our demo Spring Boot application will be narrowed to just fetching some User entities from an H2 in-memory database.

为了保持代码的简单性,我们的演示Spring Boot应用程序的功能将被缩小到只从H2内存数据库中获取一些User实体。

Spring Boot makes it easy to create repository implementations that provide minimal CRUD functionality out-of-the-box. Therefore, let’s define a simple repository interface that works with the User JPA entities:

Spring Boot使得创建存储库的实现很容易,这些存储库提供了开箱即用的最小CRUD功能。因此,让我们定义一个简单的资源库接口,与User JPA实体一起工作。

@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {}

There’s nothing inherently complex in the definition of the UserRepository interface, except that it extends PagingAndSortingRepository.

UserRepository接口的定义中并没有什么内在的复杂性,只是它扩展了PagingAndSortingRepository

This signals Spring MVC to enable automatic paging and sorting capabilities on database records.

这预示着Spring MVC能够在数据库记录上实现自动分页和排序功能

3.3. The Controller Layer

3.3.控制器层

Now, we need to implement at least a basic RESTful controller that acts as the middle tier between the client and the repository layer.

现在,我们至少需要实现一个基本的RESTful控制器,作为客户端和资源库层之间的中间层。

Therefore, let’s create a controller class, which takes a UserRepository instance in its constructor and adds a single method for finding User entities by id:

因此,让我们创建一个控制器类,在其构造函数中接受一个UserRepository实例,并添加一个方法,用于通过id查找User实体。

@RestController
public class UserController {

    @GetMapping("/users/{id}")
    public User findUserById(@PathVariable("id") User user) {
        return user;
    }
}

3.4.  Running the Application

3.4. 运行应用程序

Finally, let’s define the application’s main class and populate the H2 database with a few User entities:

最后,让我们定义应用程序的主类,用一些用户实体填充H2数据库。

@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);
        };
    }
}

Now, let’s run the application. As expected, we see the list of persisted User entities printed out to the console on startup:

现在,让我们运行该应用程序。正如预期的那样,我们看到持久化的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 Class

4.DomainClassConverter

For now, the UserController class only implements the findUserById() method.

目前,UserController类只实现了findUserById()方法。

At first sight, the method implementation looks fairly simple. But it actually encapsulates a lot of Spring Data web support functionality behind the scenes.

乍一看,该方法的实现看起来相当简单。但它实际上在幕后封装了大量的Spring Data网络支持功能。

Since the method takes a User instance as an argument, we might end up thinking that we need to explicitly pass the domain object in the request. But, we don’t.

由于该方法需要一个User实例作为参数,我们最终可能认为我们需要在请求中明确地传递域对象。但是,我们不需要。

Spring MVC uses the DomainClassConverter class to convert the id path variable into the domain class’s id type and uses it for fetching the matching domain object from the repository layer. No further lookup is necessary.

Spring MVC使用DomainClassConverterid路径变量转换成域类的id类型,并使用它从资源库层获取匹配的域对象。没有必要进一步查找。

For instance, a GET HTTP request to the http://localhost:8080/users/1 endpoint will return the following result:

例如,对http://localhost:8080/users/1端点的GET HTTP请求将返回以下结果。

{
  "id":1,
  "name":"John"
}

Hence, we can create an integration test and check the behavior of the findUserById() method:

因此,我们可以创建一个集成测试,检查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"));
    }
}

Alternatively, we can use a REST API test tool, such as Postman, to test the method.

另外,我们可以使用REST API测试工具,例如Postman,来测试该方法。

The nice thing about DomainClassConverter is that we don’t need to explicitly call the repository implementation in the controller method.

DomainClassConverter的好处是,我们不需要在控制器方法中明确调用资源库实现。

By simply specifying the id path variable, along with a resolvable domain class instance, we’ve automatically triggered the domain object’s lookup.

通过简单地指定id路径变量,以及一个可解析的域类实例,我们已经自动触发了域对象的查找

5. The PageableHandlerMethodArgumentResolver Class

5.PageableHandlerMethodArgumentResolver

Spring MVC supports the use of Pageable types in controllers and repositories.

Spring MVC支持在控制器和资源库中使用Pageable类型。

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实例是一个持有分页信息的对象。因此,当我们向控制器方法传递一个Pageable参数时,Spring MVC使用PageableHandlerMethodArgumentResolverPageable实例解析为“https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/PageRequest.html” rel=”noopener noreferrer”>PageRequest对象,这是一个简单的Pageable实现。

5.1. Using Pageable as a Controller Method Parameter

5.1.使用Pageable作为控制器方法参数

To understand how the PageableHandlerMethodArgumentResolver class works, let’s add a new method to the UserController class:

为了理解PageableHandlerMethodArgumentResolver类是如何工作的,让我们给UserController类添加一个新方法。

@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.

findUserById()方法相比,这里我们需要调用存储库的实现来获取数据库中持久化的所有User JPA实体。

Since the method takes a Pageable instance, it returns a subset of the entire set of entities, stored in a Page<User> object.

由于该方法需要一个Pageable实例,它返回整个实体集的一个子集,存储在一个Page<User>对象中。

A Page object is 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.

Page对象是一个对象列表的子列表,它暴露了几个我们可以用来检索分页结果信息的方法,包括结果页的总数,以及我们正在检索的页面的编号。

By default, Spring MVC uses the PageableHandlerMethodArgumentResolver class to construct a PageRequest object, with the following request parameters:

默认情况下,Spring MVC使用PageableHandlerMethodArgumentResolver类来构造一个PageRequest对象,其请求参数如下。

  • 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/users endpoint will return the following output:

例如,对http://localhost:8080/users端点的GET请求将返回下列输出。

{
  "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.

我们可以看到,响应包括first, pageSize, totalElements, 和totalPages JSON元素。这真的很有用,因为前端可以使用这些元素来轻松创建一个分页机制。

In addition, we can use an integration test to check the findAllUsers() method:

此外,我们可以使用集成测试来检查findAllUsers()方法。

@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

5.2.定制分页参数

In many cases, we’ll want to customize the paging parameters. The simplest way to accomplish this is by using the @PageableDefault annotation:

在许多情况下,我们会想要定制分页参数。最简单的方法是通过使用@PageableDefault注解来实现。

@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:

另外,我们可以使用PageRequestof()静态工厂方法来创建一个自定义的PageRequest对象并将其传递给repository方法。

@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.

在上面的例子中,我们创建了一个PageRequest对象的User实体,从第一页(0)开始,该页有5项。

Additionally, we can build a PageRequest object using the page and size request parameters:

此外,我们可以使用pagesize请求参数建立一个PageRequest对象。

@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:

使用这个实现,对http://localhost:8080/users?page=0&size=2端点的GET请求将返回User对象的第一页,结果页的大小将是2。

{
  "content": [
    {
      "id": 1,
      "name": "John"
    },
    {
      "id": 2,
      "name": "Robert"
    }
  ],
   
  // continues with pageable metadata
  
}

6. The SortHandlerMethodArgumentResolver Class

6、SortHandlerMethodArgumentResolver

Paging is the de-facto approach for efficiently managing large numbers of database records. But, on its own, it’s pretty useless if we can’t sort the records in some specific way.

分页是有效管理大量数据库记录的事实上的方法。但是,就其本身而言,如果我们不能以某种特定的方式对记录进行排序,那么它就很无用。

To this end, Spring MVC provides the SortHandlerMethodArgumentResolver class. The resolver automatically creates Sort instances from request parameters or from @SortDefault annotations.

为此,Spring MVC提供了SortHandlerMethodArgumentResolver类。该解析器从请求参数或@SortDefault注解中自动创建实例。

6.1. Using the sort Controller Method Parameter

6.1.使用sort控制器方法参数

To get a clear idea of how the SortHandlerMethodArgumentResolver class works, let’s add the findAllUsersSortedByName() method to the controller class:

为了清楚了解SortHandlerMethodArgumentResolver类是如何工作的,让我们在控制器类中添加findAllUsersSortedByName()方法。

@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@RequestParam("sort") String sort, Pageable pageable) {
    return userRepository.findAll(pageable);
}

In this case, the SortHandlerMethodArgumentResolver class will create a Sort object by using the sort request parameter.

在这种情况下,SortHandlerMethodArgumentResolver类将通过使用sort请求参数创建一个Sort对象。

As a result, a GET request to the http://localhost:8080/sortedusers?sort=name endpoint will return a JSON array, with the list of User objects sorted by the name property:

因此,对http://localhost:8080/sortedusers?sort=name端点的GET请求将返回一个JSON数组,其中的User对象列表按name属性排序。

{
  "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. Using the Sort.by() Static Factory Method

6.2.使用Sort.by()静态工厂方法

Alternatively, we can create a Sort object by using the Sort.by() static factory method, which takes a non-null, non-empty array of String properties to be sorted.

另外,我们可以通过使用Sort.by()静态工厂方法来创建一个Sort对象,该方法接收一个非空的array,其中包含要排序的String属性。

In this case, we’ll sort the records only by the name property:

在这种情况下,我们将只按name属性对记录进行排序。

@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName() {
    Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
    return userRepository.findAll(pageable);
}

Of course, we could use multiple properties, as long as they’re declared in the domain class.

当然,我们可以使用多个属性,只要它们是在域类中声明的。

6.3. Using the @SortDefault Annotation

6.3.使用@SortDefault注释

Likewise, we can use the @SortDefault annotation and get the same results:

同样地,我们可以使用@SortDefault注解,得到同样的结果。

@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@SortDefault(sort = "name", 
  direction = Sort.Direction.ASC) Pageable pageable) {
    return userRepository.findAll(pageable);
}

Finally, let’s create an integration test to check the method’s behavior:

最后,让我们创建一个集成测试来检查该方法的行为。

@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

7.Querydsl网络支持

As we mentioned in the introduction, Spring Data web support allows us to use request parameters in controller methods to build Querydsl‘s Predicate types and to construct Querydsl queries.

正如我们在介绍中提到的,Spring Data网络支持允许我们在控制器方法中使用请求参数来构建QuerydslPredicate类型并构建Querydsl查询。

To keep things simple, we’ll just see how Spring MVC converts a request parameter into a Querydsl BooleanExpression, which in turn is passed to a QuerydslPredicateExecutor.

为了保持简单,我们只看Spring MVC如何将请求参数转换为Querydsl BooleanExpression,它又被传递给QuerydslPredicateExecutor

To accomplish this, first we need to add the querydsl-apt and querydsl-jpa Maven dependencies to the pom.xml file:

要做到这一点,首先我们需要将querydsl-aptquerydsl-jpa Maven依赖项加入pom.xml 文件。

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>

Next, we need to refactor our UserRepository interface, which must also extend the QuerydslPredicateExecutor interface:

接下来,我们需要重构我们的UserRepository接口,它也必须扩展QuerydslPredicateExecutor接口。

@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long>,
  QuerydslPredicateExecutor<User> {
}

Finally, let’s add the following method to the UserController class:

最后,让我们在UserController类中添加以下方法。

@GetMapping("/filteredusers")
public Iterable<User> getUsersByQuerydslPredicate(@QuerydslPredicate(root = User.class) 
  Predicate predicate) {
    return userRepository.findAll(predicate);
}

Although the method implementation looks fairly simple, it actually exposes a lot of functionality beneath the surface.

虽然这个方法的实现看起来相当简单,但实际上它在表面之下暴露了很多功能。

Let’s say that we want to fetch from the database all the User entities that match a given name. We can achieve this by just calling the method and specifying a name request parameter in the URL:

假设我们想从数据库中获取所有符合给定名称的User实体。我们可以通过调用该方法并在URL中指定一个name请求参数来实现这一目的。

http://localhost:8080/filteredusers?name=John

http://localhost:8080/filteredusers?name=John

As expected, the request will return the following result:

正如预期,该请求将返回以下结果。

[
  {
    "id": 1,
    "name": "John"
  }
]

As we did before, we can use an integration test to check the getUsersByQuerydslPredicate() method:

正如我们之前所做的,我们可以使用一个集成测试来检查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"));
}

This is just a basic example of how Querydsl web support works. But it actually doesn’t reveal all of its power.

这只是Querydsl网络支持工作的一个基本例子。但它实际上并没有显示出它的全部力量。

Now, let’s say that we want to fetch a User entity that matches a given id. In such a case, we just need to pass an id request parameter in the URL:

现在,让我们假设我们想要获取一个与给定id相匹配的User实体。在这种情况下,我们只需要在URL中传递一个id请求参数

http://localhost:8080/filteredusers?id=2

http://localhost:8080/filteredusers?id=2

In this case, we’ll get this result:

在这种情况下,我们会得到这个结果。

[
  {
    "id": 2,
    "name": "Robert"
  }
]

It’s clear to see that Querydsl web support is a very powerful feature that we can use to fetch database records matching a given condition.

很明显,Querydsl网络支持是一个非常强大的功能,我们可以用它来获取符合给定条件的数据库记录。

In all the cases, the whole process boils down to just calling a single controller method with different request parameters.

在所有情况下,整个过程归结为只需调用一个具有不同请求参数的控制器方法

8. Conclusion

8.结论

In this tutorial, we took an in-depth look at Spring web support’s key components and learned how to use it within a demo Spring Boot project.

在本教程中,我们深入了解了Spring网络支持的关键组件,并学习了如何在一个演示的Spring Boot项目中使用它。

As usual, all the examples shown in this tutorial are available over on GitHub.

像往常一样,本教程中显示的所有例子都可以在GitHub上找到。