Spring Data 扩展
本节介绍了 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.
}
1 | 查找并返回匹配 Predicate 的单个实体。 |
2 | 查找并返回所有匹配 Predicate 的实体。 |
3 | 返回匹配 Predicate 的实体数量。 |
4 | 返回是否存在匹配 Predicate 的实体。 |
要使用 Querydsl 支持,请在你的 repository 接口上扩展 QuerydslPredicateExecutor
,如下例所示
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 支持
支持 repository 编程模型的 Spring Data 模块提供了各种 web 支持。Web 相关组件需要 classpath 中包含 Spring MVC JAR。其中一些甚至提供了与 Spring HATEOAS 的集成。通常,通过在 JavaConfig 配置类中使用 @EnableSpringDataWebSupport
注解来启用集成支持,如下例所示
-
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
注解注册了一些组件。我们将在本节后面讨论这些组件。它还会检测 classpath 中是否存在 Spring HATEOAS,并为之注册集成组件(如果存在)。
基础 Web 支持
上一节所示的配置注册了一些基本组件
-
一个 使用
DomainClassConverter
类,让 Spring MVC 可以从请求参数或路径变量中解析 repository 管理的域类实例。 -
HandlerMethodArgumentResolver
实现,让 Spring MVC 可以从请求参数中解析Pageable
和Sort
实例。 -
Jackson 模块,用于序列化/反序列化诸如
Point
和Distance
等类型,或特定存储相关的类型,具体取决于使用的 Spring Data 模块。
使用 DomainClassConverter
类
DomainClassConverter
类允许你在 Spring MVC 控制器方法签名中直接使用域类型,这样你就不必手动通过 repository 查找实例,如下例所示
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
该方法直接接收 User
实例,无需进一步查找。通过让 Spring MVC 首先将路径变量转换为域类的 id
类型,然后通过对为该域类型注册的 repository 实例调用 findById(…)
来访问实例,从而可以解析该实例。
目前,repository 必须实现 CrudRepository 才能符合转换为发现的条件。 |
Pageable 和 Sort 的 HandlerMethodArgumentResolvers
上一节所示的配置片段也注册了一个 PageableHandlerMethodArgumentResolver
和一个 SortHandlerMethodArgumentResolver
实例。此注册使得 Pageable
和 Sort
可以作为有效的控制器方法参数,如下例所示
@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
实例
|
要检索的页码。基于 0 索引,默认为 0。 |
|
要检索的页面大小。默认为 20。 |
|
应根据其排序的属性,格式为 |
要自定义此行为,请分别注册一个实现 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
注解来对其进行自定义。
为 Page
创建 JSON 表示
Spring MVC 控制器通常会尝试最终向客户端渲染 Spring Data 页面的表示。虽然可以直接从 handler 方法返回 Page
实例让 Jackson 按原样渲染它们,但我们强烈建议不要这样做,因为底层实现类 PageImpl
是一个域类型。这意味着我们可能出于不相关的理由想要或必须更改其 API,而此类更改可能会以破坏性的方式改变生成的 JSON 表示。
从 Spring Data 3.1 开始,我们通过发出描述该问题的警告日志来提示该问题。我们仍然最终建议利用 与 Spring HATEOAS 的集成 来以完全稳定且支持超媒体的方式渲染页面,从而轻松允许客户端导航。但从 3.3 版本开始,Spring Data 提供了一种页面渲染机制,该机制使用方便,但不需要包含 Spring HATEOAS。
使用 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
字段,公开了必要的 pagination 元数据。
全局启用简化的 Page
渲染
如果你不想更改所有现有控制器来添加返回 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);
}
}
Page 和 Slice 的超媒体支持
Spring HATEOAS 提供了一个表示模型类(PagedModel
/SlicedModel
),它允许使用必要的 Page
/Slice
元数据以及链接来丰富 Page
或 Slice
实例的内容,从而方便客户端轻松导航页面。Page
到 PagedModel
的转换由 Spring HATEOAS RepresentationModelAssembler
接口的一个实现完成,该实现称为 PagedResourcesAssembler
。类似地,可以使用 SlicedResourcesAssembler
将 Slice
实例转换为 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));
}
}
启用配置(如上例所示)允许将 PagedResourcesAssembler
用作控制器方法参数。在其上调用 toModel(…)
会产生以下效果
-
Page
的内容成为PagedModel
实例的内容。 -
PagedModel
对象附加了一个PageMetadata
实例,该实例使用Page
和底层Pageable
中的信息进行填充。 -
PagedModel
可能会根据页面的状态附加prev
和next
链接。这些链接指向方法映射到的 URI。添加到方法的 pagination 参数与PageableHandlerMethodArgumentResolver
的设置匹配,以确保稍后可以解析链接。
假设我们在数据库中有 30 个 Person
实例。你现在可以触发一个请求(GET 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 参考文档。 |
汇编器生成了正确的 URI,并获取了默认配置以将参数解析为后续请求的 Pageable
。这意味着,如果你更改该配置,链接会自动遵循该更改。默认情况下,汇编器指向调用它的控制器方法,但你可以通过传递一个自定义 Link
作为构建分页链接的基础来对其进行自定义,这会重载 PagedResourcesAssembler.toModel(…)
方法。
Spring Data Jackson 模块
核心模块以及一些特定存储模块提供了用于 Spring Data 域中使用的类型的 Jackson 模块集,例如 org.springframework.data.geo.Distance
和 org.springframework.data.geo.Point
。
一旦启用 web 支持 并且 com.fasterxml.jackson.databind.ObjectMapper
可用,就会导入这些模块。
在初始化期间,基础设施会识别 SpringDataJacksonModules
,例如 SpringDataJacksonConfiguration
,以便将声明的 com.fasterxml.jackson.databind.Module
暴露给 Jackson ObjectMapper
。
公共基础设施会注册以下域类型的数据绑定 mixin。
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
各个模块可能会提供额外的 |
Web 数据绑定支持
你可以使用 Spring Data projections(在 投影 中描述)来通过使用 JSONPath 表达式(需要 Jayway JsonPath)或 XPath 表达式(需要 XmlBeam)来绑定传入的请求 payloads,如下例所示
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
你可以将前面示例中所示的类型用作 Spring MVC handler 方法参数,或者通过在 RestTemplate
的某个方法上使用 ParameterizedTypeReference
来使用它。前面的方法声明会尝试在给定文档中的任何位置查找 firstname
。lastname
XML 查找是在传入文档的顶层执行的。它的 JSON 变体首先尝试顶层的 lastname
,如果前者没有返回值,也会尝试嵌套在 user
子文档中的 lastname
。这样,可以轻松地缓解源文档结构变化的影响,而无需客户端调用暴露的方法(这通常是基于类的 payload 绑定的一个缺点)。
支持嵌套投影,如 投影 中所述。如果方法返回复杂、非接口类型,则使用 Jackson ObjectMapper
来映射最终值。
对于 Spring MVC,只要 @EnableSpringDataWebSupport
处于活动状态且 classpath 中存在所需的依赖项,就会自动注册必要的转换器。对于与 RestTemplate
一起使用,请手动注册 ProjectingJackson2HttpMessageConverter
(JSON) 或 XmlBeamHttpMessageConverter
。
更多信息,请参阅典型的 Spring Data Examples repository 中的 web projection 示例。
Querydsl Web 支持
对于那些集成了 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
来运行它。
类型信息通常从方法的返回类型中解析。由于该信息不一定与域类型匹配,因此最好使用 QuerydslPredicate 的 root 属性。 |
以下示例展示了如何在方法签名中使用 @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 。 |
默认绑定如下所示
-
将简单属性上的
Object
作为eq
。 -
将集合类属性上的
Object
作为contains
。 -
将简单属性上的
Collection
作为in
。
你可以通过 @QuerydslPredicate
的 bindings
属性或利用 Java 8 的 default methods
并将 QuerydslBinderCustomizer
方法添加到 repository 接口来自定义这些绑定,如下所示
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 的特定 finder 方法的访问。 |
2 | 在 repository 接口上定义的 QuerydslBinderCustomizer 会自动被识别,并作为 @QuerydslPredicate(bindings=…) 的快捷方式。 |
3 | 将 username 属性的绑定定义为一个简单的 contains 绑定。 |
4 | 将 String 属性的默认绑定定义为不区分大小写的 contains 匹配。 |
5 | 从 Predicate 解析中排除 password 属性。 |
在应用 repository 或 @QuerydslPredicate 中的特定绑定之前,你可以注册一个持有默认 Querydsl 绑定的 QuerydslBinderCustomizerDefaults bean。 |
Repository Populators
如果你使用 Spring JDBC 模块,你可能熟悉使用 SQL 脚本填充 DataSource
的支持。在 repositories 层面也有类似的抽象,尽管它不使用 SQL 作为数据定义语言,因为它必须是存储无关的。因此,populators 支持 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson)来定义用于填充 repository 的数据。
假设你有一个名为 data.json
的文件,其内容如下
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
你可以使用 Spring Data Commons 中提供的 repository 命名空间的 populator 元素来填充你的 repository。要将前面数据填充到你的 PersonRepository
,请声明一个类似于以下的 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
属性来确定的。基础设施最终会选择适当的 repository 来处理已反序列化的对象。
要改为使用 XML 定义用于填充 repository 的数据,可以使用 unmarshaller-populator
元素。你可以将其配置为使用 Spring OXM 中可用的 XML marshaller 选项之一。详细信息请参阅 Spring 参考文档。以下示例展示了如何使用 JAXB 解组 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"
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>