跳到主要内容

Spring Data 扩展

ChatGPT-4o-mini 中英对照 Spring Data Extensions

本节文档记录了一组 Spring Data 扩展,旨在支持在多种上下文中使用 Spring Data。目前,大多数集成都面向 Spring MVC。

Querydsl 扩展

Querydsl 是一个框架,通过其流畅的 API 使得构建静态类型的类似 SQL 的查询成为可能。

备注

Querydsl 的维护已放缓,社区在 OpenFeign 下分叉了该项目,地址为 github.com/OpenFeign/querydsl(groupId 为 io.github.openfeign.querydsl)。Spring Data 以尽力支持的方式支持该分叉版本。

几个 Spring Data 模块通过 QuerydslPredicateExecutor 提供与 Querydsl 的集成,如下例所示:

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.
}
java
  • 查找并返回一个匹配 Predicate 的实体。

  • 查找并返回所有匹配 Predicate 的实体。

  • 返回匹配 Predicate 的实体数量。

  • 返回是否存在匹配 Predicate 的实体。

要使用 Querydsl 支持,请在您的仓库接口上扩展 QuerydslPredicateExecutor,如下例所示:

interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
java

前面的示例让你通过使用 Querydsl Predicate 实例编写类型安全的查询,如下例所示:

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

userRepository.findAll(predicate);
java

Web 支持

支持存储库编程模型的 Spring Data 模块附带各种 Web 支持。与 Web 相关的组件需要将 Spring MVC JAR 文件放入类路径中。其中一些甚至提供与 Spring HATEOAS 的集成。通常,通过在 JavaConfig 配置类中使用 @EnableSpringDataWebSupport 注解来启用集成支持,以下示例展示了这一点:

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
java

@EnableSpringDataWebSupport 注解注册了一些组件。我们将在本节稍后讨论这些组件。它还会检测类路径中的 Spring HATEOAS,并为其注册集成组件(如果存在的话)。

基本网页支持

<!-- 启用 Spring Data Web 支持 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 启用 Spring Data Web 支持 -->
<context:component-scan base-package="com.example.repository" />
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

<!-- 其他配置 -->
<!-- ... existing code ... -->
</beans>
xml

在这个代码块中,我添加了启用 Spring Data Web 支持的配置。

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

  • 使用 DomainClassConverter 类 让 Spring MVC 从请求参数或路径变量中解析由仓库管理的领域类的实例。

  • HandlerMethodArgumentResolver 实现让 Spring MVC 从请求参数中解析 PageableSort 实例。

  • Jackson 模块 用于反序列化/序列化像 PointDistance 这样的类型,或根据使用的 Spring Data 模块存储特定类型。

使用 DomainClassConverter

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

@Controller
@RequestMapping("/users")
class UserController {

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

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

该方法直接接收一个 User 实例,因此无需进一步查找。实例可以通过让 Spring MVC 首先将路径变量转换为域类的 id 类型,并最终通过调用注册在该域类型上的仓库实例的 findById(…) 方法来访问。

备注

目前,仓库必须实现 CrudRepository 才能被发现进行转换。

HandlerMethodArgumentResolvers for Pageable and Sort

前一节中显示的配置片段还注册了一个 PageableHandlerMethodArgumentResolver 以及一个 SortHandlerMethodArgumentResolver 实例。该注册使得 PageableSort 成为有效的控制器方法参数,如下例所示:

@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";
}
}
java

前面的方法签名会导致 Spring MVC 尝试通过以下默认配置从请求参数中派生一个 Pageable 实例:

表 1. 为 Pageable 实例评估的请求参数

page要检索的页面。0 为索引,默认值为 0。
size要检索的页面大小。默认值为 20。
sort需要排序的属性,格式为 `property,property(,ASC

要自定义此行为,请注册一个实现 PageableHandlerMethodArgumentResolverCustomizer 接口或 SortHandlerMethodArgumentResolverCustomizer 接口的 bean。它的 customize() 方法会被调用,让您可以更改设置,如以下示例所示:

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

如果设置现有 MethodArgumentResolver 的属性不足以满足您的需求,可以扩展 SpringDataWebConfiguration 或启用了 HATEOAS 的等效类,重写 pageableResolver()sortResolver() 方法,并导入您自定义的配置文件,而不是使用 @Enable 注解。

如果您需要从请求中解析多个 PageableSort 实例(例如,针对多个表格),您可以使用 Spring 的 @Qualifier 注解来区分它们。请求参数需要以 ${qualifier}_ 为前缀。以下示例展示了最终的方法签名:

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

你必须填充 thing1_pagething2_page,依此类推。

默认的 Pageable 传入方法相当于 PageRequest.of(0, 20),但您可以通过在 Pageable 参数上使用 @PageableDefault 注解来进行自定义。

Page 创建 JSON 表示

在 Spring MVC 控制器中,通常会尝试最终呈现 Spring Data 页面的表示给客户端。虽然可以简单地从处理方法中返回 Page 实例,让 Jackson 直接渲染它们,但我们强烈不推荐这样做,因为底层实现类 PageImpl 是一个领域类型。这意味着我们可能出于与此无关的原因需要或不得不更改它的 API,而这些更改可能会以破坏性的方式改变最终的 JSON 表示。

随着 Spring Data 3.1 的发布,我们通过发出警告日志来提示该问题。我们仍然强烈建议利用 Spring HATEOAS 集成,以便通过完全稳定且支持超媒体的方式渲染页面,从而让客户端轻松导航。但从 3.3 版本开始,Spring Data 提供了一种页面渲染机制,使用起来非常方便,但不需要包含 Spring HATEOAS。

使用 Spring Data 的 PagedModel

在其核心,支持包含了一个简化版本的 Spring HATEOAS 的 PagedModel(位于 org.springframework.data.web 包中的 Spring Data 版本)。它可以用来包装 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
}
}
java
  • Page 实例包装为 PagedModel

这将导致一个 JSON 结构如下所示:

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

注意文档包含一个 page 字段,暴露了基本的分页元数据。

全球启用简化的 Page 渲染

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

@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
class MyConfiguration { }
java

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

@Controller
class MyController {

private final MyRepository repository;

// Constructor ommitted

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

PageSlice 的超媒体支持

Spring HATEOAS 提供了一个表示模型类(PagedModel / SlicedModel),可以将 PageSlice 实例的内容与必要的 Page / Slice 元数据以及链接一起扩展,从而让客户端轻松地浏览各个页面。将 Page 转换为 PagedModel 是通过实现 Spring HATEOAS 的 RepresentationModelAssembler 接口的类 PagedResourcesAssembler 来完成的。类似地,Slice 实例可以通过 SlicedResourcesAssembler 转换为 SlicedModel。以下示例展示了如何将 PagedResourcesAssembler 作为控制器方法参数进行使用,SlicedResourcesAssembler 的工作方式完全相同:

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

启用配置,如前面的示例所示,可以将 PagedResourcesAssembler 用作控制器方法参数。在其上调用 toModel(…) 会产生以下效果:

  • Page 的内容成为 PagedModel 实例的内容。

  • PagedModel 对象会附加一个 PageMetadata 实例,并根据 Page 和底层 Pageable 的信息进行填充。

  • 根据页面的状态,PagedModel 可能会附加 prevnext 链接。这些链接指向方法映射的 URI。添加到方法中的分页参数与 PageableHandlerMethodArgumentResolver 的设置匹配,以确保稍后可以解析这些链接。

假设我们在数据库中有 30 个 Person 实例。现在你可以触发一个请求(GET [localhost:8080/people](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
}
}
javascript
注意

这里显示的 JSON 信封格式不遵循任何正式指定的结构,并且不保证稳定,我们可能会随时更改它。强烈建议启用作为超媒体支持的官方媒体类型的渲染,支持 Spring HATEOAS,例如 HAL。可以通过使用其 @EnableHypermediaSupport 注解来激活这些功能。有关更多信息,请参阅 Spring HATEOAS 参考文档

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

Spring Data Jackson 模块

核心模块以及一些特定于存储的模块,随附一组用于类型的 Jackson 模块,例如 org.springframework.data.geo.Distanceorg.springframework.data.geo.Point,这些类型被 Spring Data 域使用。
一旦启用 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 数据绑定支持

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

@ProjectedPayload
public interface UserPayload {

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

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

您可以使用前面示例中显示的类型作为 Spring MVC 处理方法的参数,或者通过在 RestTemplate 的某个方法上使用 ParameterizedTypeReference。前面的方法声明会尝试在给定文档中查找 firstnamelastname 的 XML 查找是在传入文档的顶层执行的。其 JSON 变体首先尝试顶层的 lastname,但如果前者没有返回值,也会尝试在 user 子文档中嵌套的 lastname。这样,可以轻松减轻源文档结构变化的影响,而无需客户端调用暴露的方法(这通常是基于类的有效负载绑定的一个缺点)。

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

对于 Spring MVC,只要 @EnableSpringDataWebSupport 激活,并且所需的依赖项在类路径中可用,必要的转换器会自动注册。对于与 RestTemplate 一起使用,需要手动注册 ProjectingJackson2HttpMessageConverter(JSON)或 XmlBeamHttpMessageConverter

有关更多信息,请参阅 web projection example 在经典的 Spring Data Examples repository 中。

Querydsl Web 支持

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

考虑以下查询字符串:

?firstname=Dave&lastname=Matthews
text

给定之前示例中的 User 对象,您可以通过使用 QuerydslPredicateArgumentResolver 将查询字符串解析为以下值,如下所示:

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

当在类路径中找到 Querydsl 时,功能会自动启用,并且会启用 @EnableSpringDataWebSupport

在方法签名中添加 @QuerydslPredicate 提供了一个现成可用的 Predicate,你可以通过使用 QuerydslPredicateExecutor 来运行它。

提示

类型信息通常是从方法的返回类型中解析的。由于该信息不一定与领域类型匹配,因此使用 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";
}
}
java
  • 解析查询字符串参数以匹配 Predicate 以获取 User

默认绑定如下:

  • Object 在简单属性上使用 eq

  • Object 在类似集合的属性上使用 contains

  • Collection 在简单属性上使用 in

您可以通过 @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
}
}
java
  • QuerydslPredicateExecutor 提供对 Predicate 的特定查找方法的访问。

  • 在仓库接口上定义的 QuerydslBinderCustomizer 会自动被识别,并简化 @QuerydslPredicate(bindings=…​)

  • username 属性的绑定定义为简单的 contains 绑定。

  • String 属性的默认绑定定义为不区分大小写的 contains 匹配。

  • Predicate 解析中排除 password 属性。

提示

您可以注册一个 QuerydslBinderCustomizerDefaults bean 来保存默认的 Querydsl 绑定,然后再应用来自仓库或 @QuerydslPredicate 的特定绑定。

仓库填充器

如果你使用 Spring JDBC 模块,那么你可能熟悉使用 SQL 脚本填充 DataSource 的支持。在仓库层级上也有类似的抽象,尽管它不使用 SQL 作为数据定义语言,因为它必须是与存储无关的。因此,填充器支持 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson),以定义用于填充仓库的数据。

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

[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
javascript

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

<?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>
xml

前面的声明导致 data.json 文件被 Jackson 的 ObjectMapper 读取和反序列化。

JSON 对象被反序列化到的类型是通过检查 JSON 文档的 _class 属性来确定的。基础设施最终选择合适的存储库来处理被反序列化的对象。

要改为使用 XML 来定义应填充的数据仓库,可以使用 unmarshaller-populator 元素。你可以将其配置为使用 Spring OXM 中提供的 XML marshaller 选项之一。有关详细信息,请参见 Spring 参考文档。以下示例演示了如何使用 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>
xml