跳到主要内容

Spring Data 扩展

DeepSeek V3 中英对照 Spring Data Extensions

本节记录了一组 Spring Data 扩展,这些扩展使得 Spring Data 可以在各种上下文中使用。目前,大部分集成主要针对 Spring MVC。

Querydsl 扩展

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

备注

Querydsl 的维护速度已经放缓,社区已在 OpenFeign 下 fork 了该项目,地址为 github.com/OpenFeign/querydsl(groupId 为 io.github.openfeign.querydsl)。Spring Data 将尽最大努力支持该 fork 版本。

多个 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

使用 Querydsl 实现类型安全的查询方法

MongoDB 仓库及其响应式对应物与 Querydsl 项目集成,该项目提供了一种执行类型安全查询的方式。

与将查询编写为内联字符串或将其外部化到 XML 文件中不同,它们是通过流畅的 API 构建的。

— Querydsl 团队

它提供了以下功能:

  • IDE 中的代码补全功能(所有属性、方法和操作都可以在您喜欢的 Java IDE 中展开)。

  • 几乎不允许语法无效的查询(在所有层次上都是类型安全的)。

  • 可以安全地引用领域类型和属性——不涉及字符串!

  • 更好地适应领域类型中的重构更改。

  • 增量查询定义更容易。

请参阅 QueryDSL 文档 了解如何使用 Maven 或 Ant 为基于 APT 的代码生成引导您的环境。

QueryDSL 允许您编写如下查询:

QPerson person = QPerson.person;
List<Person> result = repository.findAll(person.address.zipCode.eq("C0123"));

Page<Person> page = repository.findAll(person.lastname.contains("a"),
PageRequest.of(0, 2, Direction.ASC, "lastname"));
java

QPerson 是一个由 Java 注解处理器生成的类。有关如何在使用构建系统时设置注解处理,请参阅 设置注解处理。它是一个 Predicate,允许你编写类型安全的查询。请注意,查询中除了 C0123 值外,没有其他字符串。

你可以通过使用 QuerydslPredicateExecutor / ReactiveQuerydslPredicateExecutor 接口来使用生成的 Predicate 类,如下面的代码清单所示:

public interface QuerydslPredicateExecutor<T> {

Optional<T> findOne(Predicate predicate);

List<T> findAll(Predicate predicate);

List<T> findAll(Predicate predicate, Sort sort);

List<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);

Page<T> findAll(Predicate predicate, Pageable pageable);

List<T> findAll(OrderSpecifier<?>... orders);

long count(Predicate predicate);

boolean exists(Predicate predicate);

<S extends T, R> R findBy(Predicate predicate, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}
java

要在你的仓库实现中使用这个功能,将其添加到你的接口继承的仓库接口列表中,如下例所示:

interface PersonRepository extends MongoRepository<Person, String>, QuerydslPredicateExecutor<Person> {

// 额外的查询方法放在这里
}
java

设置注解处理

要在 Spring Data MongoDB 中使用 Querydsl,你需要在构建系统中设置注解处理,以生成 Q 类。虽然你可以手动编写 Q 类,但建议使用 Querydsl 注解处理器为你生成这些类,以确保 Q 类与你的领域模型保持同步。

Spring Data MongoDB 自带了一个注解处理器 MongoAnnotationProcessor,但默认情况下并未注册。通常,注解处理器是通过 Java 的服务加载器通过 META-INF/services/javax.annotation.processing.Processor 进行注册的,一旦它们出现在类路径中,这些处理器就会被激活。大多数 Spring Data 用户并不使用 Querydsl,因此对于不会从 Querydsl 中受益的项目来说,要求额外的强制依赖是没有意义的。因此,你需要在构建系统中激活注解处理。

以下示例展示了如何在 Maven 和 Gradle 中通过提及依赖项和编译器配置更改来设置注解处理:

<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-mongodb</artifactId>
<version>${querydslVersion}</version>
<classifier>jakarta</classifier>

<!-- Recommended: Exclude the mongo-java-driver to avoid version conflicts -->
<exclusions>
<exclusion>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydslVersion}</version>
<classifier>jakarta</classifier>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessors>
<annotationProcessor>
org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor
</annotationProcessor>
</annotationProcessors>

<!-- Recommended: Some IDE's might require this configuration to include generated sources for IDE usage -->
<generatedTestSourcesDirectory>target/generated-test-sources</generatedTestSourcesDirectory>
<generatedSourcesDirectory>target/generated-sources</generatedSourcesDirectory>
</configuration>
</plugin>
</plugins>
</build>
xml

请注意,上述设置展示了最简单的用法,省略了项目可能需要的任何其他选项或依赖项。

Web 支持

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

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
java

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

基本 Web 支持

在 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 才有资格被发现以进行转换。

用于 Pageable 和 Sort 的 HandlerMethodArgumentResolvers

上一节中展示的配置片段还注册了一个 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|DESC)(,IgnoreCase)。默认排序方向为区分大小写的升序。如果要切换方向或区分大小写,请使用多个 sort 参数 — 例如,?sort=firstname&sort=lastname,asc&sort=city,ignorecase。 |

要自定义此行为,分别注册一个实现了 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),它允许用必要的 Page/Slice 元数据以及链接来丰富 PageSlice 实例的内容,从而使客户端能够轻松地导航页面。将 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 信封格式并未遵循任何正式指定的结构,因此不能保证其稳定性,我们可能会随时对其进行更改。强烈建议启用渲染作为支持超媒体的官方媒体类型,例如 HAL,该类型由 Spring HATEOAS 支持。可以通过使用其 @EnableHypermediaSupport 注解来激活这些功能。更多信息请参阅 Spring HATEOAS 参考文档

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

Spring Data Jackson 模块

核心模块以及一些特定于存储的模块,附带了一组用于类型的 Jackson 模块,例如 org.springframework.data.geo.Distanceorg.springframework.data.geo.Point,这些类型由 Spring Data 领域使用。
一旦启用了 web 支持 并且 com.fasterxml.jackson.databind.ObjectMapper 可用,这些模块就会被导入。

在初始化过程中,SpringDataJacksonModulesSpringDataJacksonConfiguration 会被基础设施自动识别,从而使得声明的 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 的投影(在投影中有详细描述)来绑定传入的请求负载,通过使用 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。通过这种方式,可以轻松缓解源文档结构的变化,而无需客户端调用暴露的方法(这通常是基于类的有效负载绑定的一个缺点)。

嵌套投影功能在投影文档中有详细描述。如果方法返回的是一个复杂的非接口类型,将使用 Jackson 的 ObjectMapper 来映射最终值。

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

更多信息,请参见 Spring Data Examples 仓库 中的 web 投影示例

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
  • 将查询字符串参数解析为匹配的 UserPredicate

默认绑定如下:

  • 在简单属性上使用 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 匹配。

  • password 属性从 Predicate 解析中排除。

提示

在应用来自存储库或 @QuerydslPredicate 的特定绑定之前,你可以注册一个 QuerydslBinderCustomizerDefaults bean 来保存默认的 Querydsl 绑定。

仓库填充器

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

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

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

你可以使用 Spring Data Commons 中提供的 repository 命名空间的填充器元素来填充你的仓库。要将上述数据填充到你的 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 编组器选项之一。详情请参阅 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