原创

Spring Data Web支持

1.概述

Spring MVCSpring Data 都用他们的方式在简化应用程序开发做的很棒。但是,如果我们将他们组合在一起呢?

在本教程中,我们将查看 Spring Data的Web支持 以及它的解析器如何减少样板文件并使我们的controller更有表现力。

在此过程中,我们将查看Querydsl及其与Spring Data的集成。

2.背景

Spring DataWeb支持是一组在标准Spring MVC平台上实现的与Web相关的功能,目的是为controller层添加额外的功能。

Spring Data web支持的功能是围绕几个解析器类构建的。解析器简化了和 Spring Data repository交互的controller方法的实现,还使用额外的功能增强了他们。

这些功能包括从repository层获取域对象,而不需要显式调用repository实现,以及构建可以作为支持分页和排序的数据片段发送到客户端的controller响应。

此外,对于带有一个或多个请求参数到controller方法的请求可以在内部被解析为 Querydsl查询。

3.用Spring Boot项目演示

要了解我们如何使用Spring Data web支持来改进我们的controller的功能,让我们创建一个基本的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 controllerspring-boot-starter-jpa用于实现持久层,以及spring-boot-starter-test用于测试controller API

由于我们将采用H2 作为底层数据库,我们也引入了com.h2database

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

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

3.1.domain类

现在,让我们添加一个简单的 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.repository层

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

Spring Boot可以轻松创建提供开箱即用的最小CRUD功能的repository 实现。因此,让我们定义一个和 User JPA实体一起运行的简单的repository 接口:

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

除了继承 PagingAndSortingRepository 之外,UserRepository 接口的定义一点也不复杂。

这表示 Spring MVC启用了对数据库记录自动分页和排序功能。

3.3.controller层

现在,我们至少需要实现一个基础的RESTful controller,它充当客户端和 repository 层间的中间层。

因此,让我们创建一个 controller 类,它在其构造器中获取UserRepository实例并添加一个方法来通过id查找User实体:

@RestController
public class UserController {

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

3.4.运行应用程序

最后,让我们定义应用程序的主类,并使用一些User实体填充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);
        };
    }
}

现在,让我们运行应用程序。和预期的一样,我们在启动时看到打印到控制台的持久化的 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.DomainClassConverter类

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

初看之下,方法实现看起来相当简单。但它在幕后实际封装了许多Spring Data web支持的功能。由于该方法将User实例作为参数,我们最终可能任务我们需要在请求中显示传递domain 对象,但我们没有。

Spring MVC 使用 DomainClassConverter 类转换id路径变量到domain类的 id类型并使用它从 repository层获取匹配的domain对象,无需进一步查找。例如,http://localhost:8080/users/1 端点(endpoint)的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 的好处是我们不需要在controller 方法中显式的调用 repository实现,通过简单的指定 id路径变量以及可解析的 domain类实例,我们已经触发了domain对象的查找。

5.PageableHandlerMethodArgumentResolver类

Spring MVC支持在controllerrepository 中使用 Pageable类型。

简单来说,Pageable实例是一个保存分页信息的对象。因此,当我们传递Pageable参数给controller方法,Spring MVC使用 PageableHandlerMethodArgumentResolver类将Pageable实例解析为 PageRequest对象,这是一个简单的Pageable实现。

5.1.使用Pageable作为controller方法参数

要理解 PageableHandlerMethodArgumentResolver 类如何运行。让我们添加一个新方法到UserController 类:

@GetMapping("/users")
public Page<User> findAllUsers(Pageable pageable) {
    return userRepository.findAll(pageable);
}

findUserById() 方法作为对照,这里我们需要调用 repository实现来或者数据库中持久化的所有User JPA实体。由于该方法使用了Pageable实例,因此它返回存储在Page<User> 对象中实体集的子集。

Page对象是我们可以用于检索有关分页结果信息的对象列表的子列表对象,包括结果总页数和我们正在检索的页数。

默认情况下,Spring MVC使用 PageableHandlerMethodArgumentResolver 类构造带有一下请求参数的 PageRequest对象:

  • page:我们要检索的页数——该参数从 0 开始且它的默认值为 0
  • size:我们要检索的页面的内容数量——默认值是 20
  • sort:我们可以使用一个或多个属性对结果排序,使用以下格式:property1,property2(,asc|desc) ——举个例子,?sort=name&sort=email,asc

例如,http://localhost:8080/users端点(endpoint)的 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
}

我们可以看到,响应包括 firstpageSizetotalElementstotalPages JSON 元素。这非常有用,因为前端可以使用这些元素轻松创建分页机制。

另外,我们可以使用集成测试来检查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.自定义分页参数

在许多情况下,我们需要自定义分页参数。完成此操作的最简单方法是使用 @PageableDefault注解:

@GetMapping("/users")
public Page<User> findAllUsers(@PageableDefault(value = 2, page = 0) Pageable pageable) {
    return userRepository.findAll(pageable);
}

或者,我们可以使用 PageRequestof() 静态工厂方法创建自定义PageRequest对象并将其传递给repository方法:

@GetMapping("/users")
public Page<User> findAllUsers() {
    Pageable pageable = PageRequest.of(0, 5);
    return userRepository.findAll(pageable);
}

第一个参数是从 0 开始的页数,第二个是我们希望检索的页面大小。

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

另外,我们可以使用pagesize请求参数构建PageRequest对象:

@GetMapping("/users")
public Page<User> findAllUsers(@RequestParam("page") int page, 
  @RequestParam("size") int size, Pageable pageable) {
    return userRepository.findAll(pageable);
}

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

{
  "content": [
    {
      "id": 1,
      "name": "John"
    },
    {
      "id": 2,
      "name": "Robert"
    }
  ],

  // continues with pageable metadata

}

6.SortHandlerMethodArgumentResolver类

分页是有效管理大量数据库记录的业界标准做法。但是,就其本身而言,如果我们不能以某种特定方式对记录进行排序那将毫无用处。

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

6.1.使用 sort controller 方法参数

为了弄清楚 SortHandlerMethodArgumentResolver类如何运作,让我们添加 findAllUsersSortedByName() 方法到controller类:

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

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

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

{
  "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() 静态工厂方法

或者,我们可以使用Sort.by()静态工厂方法创建一个Sort对象,该方法接受一个非null,非空(empty)的用于排序的String属性的数组。

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

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

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

6.3.使用@SortDefault注解

同样,我们可以使用 @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支持

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

为了简单起见,我们将看到Spring MVC 如何将请求参数转换为Querydsl BooleanExpression,然后传递给QuerydslPredicateExecutor

为此,我们首先需要添加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>

接下来,我们需要重构我们的 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);
}

尽管方法实现看起来非常简单,但它实际上暴露了许多表面之下的功能。

假设我们想要从数据库中获取匹配给定name的所有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支持如何运行的一个基础示例。但它实际上没有暴露出它的所有能力。

现在,假设我们希望获取匹配给定idUser实体。在这种情况下,我们只需要在URL中传递 id请求参数:

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

在这种情况下,我们将得到这个结果:

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

很明显,Querydsl web支持是一个非常强大的功能,我们可以使用它来获取匹配给定条件的数据库记录。在所有情况下,整个过程归结为只调用具有不同请求参数的单个 controller 方法。

8.总结

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

正文到此结束