Spring Data Commons 提供 PageRepository 扩展,允许将分页支持轻松添加到 Spring Data 存储库中。该扩展消除了手动创建分页查询的需要,并提供了用于创建和操作分页结果的便捷方法,从而简化了分页管理并提高了代码的可读性。

Spring Data Extensions

  • Querydsl 支持,允许通过流畅的 API 构建类型安全 SQL 查询。

  • Web 支持,包括以下功能:

  • 使用域类转换器在控制器方法签名中直接使用域类型。

  • 解析 Pageable 和 Sort 实例,并允许自定义默认配置。

  • 创建 JSON 页面的表示形式,以呈现 Spring Data 页面的内容。

  • 超媒体支持,允许将链接添加到页面的表示形式中,以便轻松浏览。

  • Spring Data Jackson 模块,用于绑定和反绑定 Spring Data 类型的 JSON 表示形式。

  • Web 数据绑定支持,允许使用 JSONPath 或 XPath 表达式将传入请求有效负载绑定到 Spring MVC 处理程序方法参数。

  • Querydsl Web 支持,允许从请求查询字符串中包含的属性派生查询。

  • 存储库填充器,用于使用 JSON 或 XML 数据填充存储库。

本节记录了一组 Spring Data 扩展,它能够在各种上下文中使用 Spring Data。当前,大部分集成都针对 Spring MVC。

Querydsl Extension

Querydsl 是一个框架,通过其流畅 API 启用静态类型 SQL 式查询的构建。

多个 Spring Data 模块都通过“QuerydslPredicateExecutor”提供与 Querydsl 的集成,如下面的示例所示:

QuerydslPredicateExecutor interface
public interface QuerydslPredicateExecutor<T> {

  Optional<T> findById(Predicate predicate);  1

  Iterable<T> findAll(Predicate predicate);   2

  long count(Predicate predicate);            3

  boolean exists(Predicate predicate);        4

  // … more functionality omitted.
}
1 找到并返回匹配 Predicate 的单个实体。
2 找到并返回匹配 Predicate 的所有实体。
3 返回匹配 Predicate 的实体数。
4 返回是否匹配 Predicate 的实体存在。

如以下示例所示,要使用 Querydsl 支持,请在你的存储库接口上扩展“QuerydslPredicateExecutor”:

Querydsl integration on repositories
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

上述示例允许你使用 Querydsl “Predicate”实例编写类型安全查询,如下例所示:

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
	.and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

Web support

支持存储库编程模型的 Spring Data 模块随各种 Web 支持一起提供。Web 相关组件需要 Spring MVC JAR 在类路径上。其中一些甚至提供与 Spring HATEOAS 的集成。通常,通过在 JavaConfig 配置类中使用 @EnableSpringDataWebSupport 注释启用集成支持,如以下示例所示:

Enabling Spring Data web support
  • Java

  • XML

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />

@EnableSpringDataWebSupport”注释会注册一些组件。我们将在本节后面讨论这些组件。它还检测类路径上的 Spring HATEOAS,以及为其注册集成组件(如果存在)。

Basic Web Support

Enabling Spring Data web support in XML

previous section 中显示的配置注册了一些基本组件:

Using the DomainClassConverter Class

DomainClassConverter”类允许你在 Spring MVC 控制器方法签名中直接使用域类型,这样你就不必通过存储库手动查找实例,如下例所示:

A Spring MVC controller using domain types in method signatures
@Controller
@RequestMapping("/users")
class UserController {

  @RequestMapping("/{id}")
  String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}

此方法直接接收“用户”实例,无需进一步查找。可以通过让 Spring MVC 将路径变量转换为域类别的“id”类型,然后最终通过调用域类型注册的存储库实例的“findById(…)”来访问该实例来解析该实例。

目前,仓库必须实现 CrudRepository,才有资格转换。

HandlerMethodArgumentResolvers for Pageable and Sort

previous section 中显示的配置片段还注册了一个 PageableHandlerMethodArgumentResolver 和一个 SortHandlerMethodArgumentResolver 的实例。此注册启用 PageableSort 作为有效的控制器方法参数,如以下示例所示:

Using Pageable as a controller method argument
@Controller
@RequestMapping("/users")
class UserController {

  private final UserRepository repository;

  UserController(UserRepository repository) {
    this.repository = repository;
  }

  @RequestMapping
  String showUsers(Model model, Pageable pageable) {

    model.addAttribute("users", repository.findAll(pageable));
    return "users";
  }
}

前面的方法签名导致 Spring MVC 尝试使用以下默认配置从请求参数中派生“Pageable”实例:

Table 1. Request parameters evaluated for Pageable instances

page

您要检索的页面。以0为起始索引,默认为0。

size

您要检索的页面的大小的。默认为 20。

sort

应当通过 `property,property(,ASC

为了自定义此行为,请注册一个分别实现“PageableHandlerMethodArgumentResolverCustomizer”接口或“SortHandlerMethodArgumentResolverCustomizer”接口的 bean。调用其“customize()”方法,让你更改设置,如下例所示:

@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
    return s -> s.setPropertyDelimiter("<-->");
}

如果设置现有“MethodArgumentResolver”的属性不足以满足你的目的,请扩展“SpringDataWebConfiguration”或启用 HATEOAS 的等效项,覆盖“pageableResolver()”或“sortResolver()”方法,并导入你的自定义配置文件,而不是使用“@Enable”注解。

如果你需要从请求中解析多个“Pageable”或“Sort”实例(例如,对于多个表格),则可以使用 Spring 的“@Qualifier”注解来区分它们。然后,请求参数必须以“${qualifier}_”作为前缀。以下示例显示结果方法签名:

String showUsers(Model model,
      @Qualifier("thing1") Pageable first,
      @Qualifier("thing2") Pageable second) { … }

你必须填充“thing1_page”、“thing2_page”等等。

传递到方法中的默认“Pageable”等效于“PageRequest.of(0, 20)”,但你可以通过在“Pageable”参数上使用“@PageableDefault”注解对其进行自定义。

Creating JSON representations for Page

对于 Spring MVC 控制器来说,尝试最终向客户端呈现 Spring Data 页面的表示形式是很常见的。虽然人们可以简单地从处理程序方法返回“Page”实例,让 Jackson 按原样呈现它们,但我们强烈建议不要这样做,因为底层实现类“PageImpl”是一个域类型。这意味着我们可能出于无关的原因想要或不得不想更改其 API,并且此类更改可能会以中断的方式改变结果 JSON 表示形式。

有了 Spring Data 3.1,我们开始通过发出描述该问题的警告日志来暗示这个问题。我们仍然最终建议利用 the integration with Spring HATEOAS 来获得完全稳定且支持超媒体的方式来呈现页面,以便客户端可以轻松地导航。但从版本 3.3 开始,Spring Data 提供了一种方便使用的页面呈现机制,但不需要包含 Spring HATEOAS。

Using Spring Data' PagedModel

从本质上讲,该支持包括 Spring HATEOAS 的“PagedModel”的一个简化版本(Spring Data 位于“org.springframework.data.web”包中)。它可用于包装“Page”实例,并产生一个简化表示形式,该表示形式反映了 Spring HATEOAS 确立的结构,但省略了导航链接。

import org.springframework.data.web.PagedModel;

@Controller
class MyController {

  private final MyRepository repository;

  // Constructor ommitted

  @GetMapping("/page")
  PagedModel<?> page(Pageable pageable) {
    return new PagedModel<>(repository.findAll(pageable)); (1)
  }
}
1 Page 实例封装成一个 PagedModel

这将产生如下所示的 JSON 结构:

{
  "content" : [
     … // Page content rendered here
  ],
  "page" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

请注意,该文档包含一个“page”字段,该字段公开基本分页元数据。

Globally enabling simplified Page rendering

如果你不想更改所有现有控制器以添加映射步骤来返回“PagedModel”而不是“Page”,则可以通过调整“@EnableSpringDataWebSupport”来启用将“PageImpl”实例自动转换为“PagedModel”:

@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
class MyConfiguration { }

这将允许你的控制器仍然返回“Page”实例,并且它们将自动呈现为简化表示形式:

@Controller
class MyController {

  private final MyRepository repository;

  // Constructor ommitted

  @GetMapping("/page")
  Page<?> page(Pageable pageable) {
    return repository.findAll(pageable);
  }
}

Hypermedia Support for Page and Slice

Spring HATEOAS 提供了一个表示模型类(“PagedModel”/“SlicedModel”),它允许使用必要的“Page”/“Slice”元数据以及链接(让客户端可以轻松地浏览页面)来丰富“Page”或“Slice”实例的内容。“Page”到“PagedModel”的转换是由 Spring HATEOAS“RepresentationModelAssembler”接口的实现完成的,称为“PagedResourcesAssembler”。类似地,“Slice”实例可以使用“SlicedResourcesAssembler”转换为“SlicedModel”。以下示例显示如何将“PagedResourcesAssembler”用作控制器方法参数,因为“SlicedResourcesAssembler”的工作方式完全相同:

Using a PagedResourcesAssembler as controller method argument
@Controller
class PersonController {

  private final PersonRepository repository;

  // Constructor omitted

  @GetMapping("/people")
  HttpEntity<PagedModel<Person>> people(Pageable pageable,
    PagedResourcesAssembler assembler) {

    Page<Person> people = repository.findAll(pageable);
    return ResponseEntity.ok(assembler.toModel(people));
  }
}

启用配置,如前面的示例所示,可以让“PagedResourcesAssembler”用作控制器方法参数。对其调用“toModel(…)”有以下影响:

  • Page 的内容将变为 PagedModel 实例的内容。

  • PagedModel 对象获取一个附加的 PageMetadata 实例,并利用 Page 和底层的 Pageable 中的信息填充该实例。

  • PagedModel 可能会附加 prevnext 链接,具体取决于页面的状态。这些链接指向 API 将方法映射到的 URI。添加到该方法的分页参数与 PageableHandlerMethodArgumentResolver 的设置匹配,以确保以后可以解析这些链接。

假设我们在数据库中有 30 个 Person 实例。你现在可以触发一个请求 (GET [role="bare"]http://localhost:8080/people),然后看到类似于以下内容的输出:

{ "links" : [
    { "rel" : "next", "href" : "http://localhost:8080/persons?page=1&size=20" }
  ],
  "content" : [
     … // 20 Person instances rendered here
  ],
  "page" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

这里显示的 JSON 封套格式不遵循任何正式指定的结构,并且不能保证其稳定,我们可能会随时更改它。强烈建议启用 Spring HATEOAS 支持的超媒体、官方媒体类型(如 HAL)。可以使用其 @EnableHypermediaSupport 注解激活它们。在 Spring HATEOAS reference documentation 中查找更多信息。

该汇编器生成了正确的 URI,并且还采用了默认配置将参数解析成“Pageable”以用于即将到来的请求。这意味着,如果你更改该配置,链接将自动遵循更改。默认情况下,该汇编器指向调用它的控制器方法,但你可以通过传递一个自定义的“Link”作为构建分页链接的基础来对其进行自定义,这会重载“PagedResourcesAssembler.toModel(…)”方法。

Spring Data Jackson Modules

核心模块以及一些特定于存储的模块随用于 Spring Data 域的类型(如 org.springframework.data.geo.Distanceorg.springframework.data.geo.Point)的一组 Jackson 模块一起提供。一旦启用 web support 且获得 com.fasterxml.jackson.databind.ObjectMapper 时,便会导入这些模块。

在初始化期间,SpringDataJacksonModules(如 SpringDataJacksonConfiguration)将被基础架构选中,以便声明的 com.fasterxml.jackson.databind.Module 可供 Jackson ObjectMapper 使用。

常见的基础架构为以下域类型注册了数据绑定混入。

org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon

各个模块可能提供其他 SpringDataJacksonModules。有关更多详细信息,请参阅特定存储部分。

Web Databinding Support

您可以使用 Spring Data 预测(在 Projections 中描述)通过使用 JSONPath 表达式(需要 Jayway JsonPath)或 XPath 表达式(需要 XmlBeam)来绑定传入请求有效负载,如以下示例所示:

HTTP payload binding using JSONPath or XPath expressions
@ProjectedPayload
public interface UserPayload {

  @XBRead("//firstname")
  @JsonPath("$..firstname")
  String getFirstname();

  @XBRead("/lastname")
  @JsonPath({ "$.lastname", "$.user.lastname" })
  String getLastname();
}

您可以将前一个示例中所示的类型用作 Spring MVC 处理程序方法参数,或在 RestTemplate 方法之一上使用 ParameterizedTypeReference。前述方法声明将尝试在给定文档中的任何位置查找 firstnamelastname XML 查找在传入文档的顶级上执行。该 JSON 变体首先尝试顶级 lastname,但如果前者未返回任何值,它还会尝试嵌套在 user 子文档中的 lastname。这种方式可以轻松缓解源文档的结构变化,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。

嵌套投影受支持,如 Projections 中所述。如果该方法返回一个复杂的非接口类型,则 Jackson ObjectMapper 用于映射最终值。

对于 Spring MVC,只要 @EnableSpringDataWebSupport 处于活动状态且类路径中存在所需依赖项,就会自动注册必要的转换器。如需与 RestTemplate 配合使用,请手动注册 ProjectingJackson2HttpMessageConverter(JSON)或 XmlBeamHttpMessageConverter

有关更多信息,请参阅授权 Spring Data Examples repository 中的 web projection example

Querydsl Web Support

对于那些具有 QueryDSL 集成的存储,您可以从 Request 查询字符串中包含的属性中派生查询。

请考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

鉴于来自上一个示例的 User 对象,您可以使用 QuerydslPredicateArgumentResolver 将查询字符串解析为以下值,如下所示:

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))

当 classpath 中找到 Querydsl 时,将自动启用此功能,并启用 @EnableSpringDataWebSupport

向方法签名中添加 @QuerydslPredicate 提供一个现成的 Predicate,您可以使用 QuerydslPredicateExecutor 运行该 Predicate

类型信息通常从方法的返回类型解析。由于该信息不一定与域类型匹配,因此,最好使用 QuerydslPredicateroot 属性。

以下示例演示如何在方法签名中使用 @QuerydslPredicate

@Controller
class UserController {

  @Autowired UserRepository repository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,    1
          Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {

    model.addAttribute("users", repository.findAll(predicate, pageable));

    return "index";
  }
}
1 解析查询字符串参数,以获取与 User 匹配的 Predicate

默认绑定如下:

  • eq 的简单属性使用 Object

  • contains 的集合式属性使用 Object

  • in 的简单属性使用 Collection

您可以通过 @QuerydslPredicatebindings 属性来自定这些绑定,或通过使用 Java 8 default methods 并向存储库接口添加 QuerydslBinderCustomizer 方法来自定这些绑定,如下所示:

interface UserRepository extends CrudRepository<User, String>,
                                 QuerydslPredicateExecutor<User>,                1
                                 QuerydslBinderCustomizer<QUser> {               2

  @Override
  default void customize(QuerydslBindings bindings, QUser user) {

    bindings.bind(user.username).first((path, value) -> path.contains(value))    3
    bindings.bind(String.class)
      .first((StringPath path, String value) -> path.containsIgnoreCase(value)); 4
    bindings.excluding(user.password);                                           5
  }
}
1 QuerydslPredicateExecutor 提供对 Predicate 特定查找器方法的访问权限。
2 在存储库接口中定义的 QuerydslBinderCustomizer 会自动选取并简化 @QuerydslPredicate(bindings=&#8230;&#8203;)
3 定义 username 属性的绑定为一个简单的 contains 绑定。
4 定义 String 属性的默认绑定为一个不区分大小写的 contains 匹配。
5 Predicate 解析中排除 password 属性。

您可以在从存储库或 @QuerydslPredicate 应用特定绑定前,注册保存默认 Querydsl 绑定的 QuerydslBinderCustomizerDefaults bean。

Repository Populators

如果您使用 Spring JDBC 模块,您可能熟悉利用 SQL 脚本填充 DataSource 的支持。存储库级别上提供类似的抽象,尽管它未使用 SQL 作为数据定义语言,因为它必须独立于存储。因此,填充器支持 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson)定义填充存储库的数据。

假设您有一个名为 data.json 的文件,内容如下:

Data defined in JSON
[ { "_class" : "com.acme.Person",
 "firstname" : "Dave",
  "lastname" : "Matthews" },
  { "_class" : "com.acme.Person",
 "firstname" : "Carter",
  "lastname" : "Beauford" } ]

您可以使用 Spring Data Commons 中提供的存储库命名空间的填充器元素填充您的存储库。要将前述数据填充到您的 PersonRepository 中,请声明类似于以下内容的填充器:

Declaring a Jackson repository populator
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd">

  <repository:jackson2-populator locations="classpath:data.json" />

</beans>

以上声明将导致读取 data.json 文件并由 Jackson ObjectMapper 反序列化该文件。

反序列化 JSON 对象的类型由检查 JSON 文档中的 _class 属性决定。基础架构最终会选择适当的存储库为已反序列化的对象处理。

要使用 XML 定义数据,以便刷新那些存储库,您可以使用 unmarshaller-populator 元素。您可以将其配置为使用 Spring OXM 中的一个 XML 转换器选项。有关详细信息,请参阅 {spring-framework-docs}/data-access/oxm.html[Spring 参考手册]。以下示例显示了如何用 JAXB 反组装一个存储库填充器:

Declaring an unmarshalling repository populator (using JAXB)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    https://www.springframework.org/schema/data/repository/spring-repository.xsd
    http://www.springframework.org/schema/oxm
    https://www.springframework.org/schema/oxm/spring-oxm.xsd">

  <repository:unmarshaller-populator locations="classpath:data.json"
    unmarshaller-ref="unmarshaller" />

  <oxm:jaxb2-marshaller contextPath="com.acme" />

</beans>