© 2008-2022 The original authors.
Spring Data 是 Spring 框架的一个子项目,旨在简化与各种数据存储技术(如关系型数据库、NoSQL数据库、图数据库等)的集成和操作。它提供了一种统一的编程模型和API,使开发人员能够以一致的方式访问和操作不同类型的数据存储。Spring Data 通过提供通用的 CRUD 操作、查询方法、事务管理和数据访问抽象层等功能,简化了数据访问层的开发工作。它还提供了与 Spring 框架其他模块(如Spring Boot、Spring MVC等)的无缝集成,使开发人员能够更轻松地构建全栈应用程序。
| 本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
序言
1. Project Metadata
-
版本管理: https://github.com/spring-projects/spring-data-commons
-
BUG跟踪: https://github.com/spring-projects/spring-data-commons/issues
-
Release repository: https://repo.spring.io/libs-release
-
Milestone repository: https://repo.spring.io/libs-milestone
-
Snapshot repository: https://repo.spring.io/libs-snapshot
参考文档
2. 依赖
由于各个Spring Data模块的起始日期不同,它们中的大多数都有不同的主要和次要版本号。找到兼容的模块最简单的方法是依靠Spring Data Release Train BOM,我们在发行时定义了兼容版本。在Maven项目中,你可以在POM的 <dependencyManagement /> 部分声明这一依赖,如下所示。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2023.0.0-SNAPSHOT</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
当前发布的 train version 是 2023.0.0-SNAPSHOT。train version 使用模式为 YYY.MINOR.MICRO 的 calver。对于GA版本和服务版本,版本名称遵循 ${calver},对于所有其他版本,版本名称遵循以下模式:${calver}-${modifier},其中 modifier 可以是以下之一。
-
SNAPSHOT: 当前快照 -
M1,M2, 以此类推: 里程碑 -
RC1,RC2, 以此类推: 候选发布
你可以在我们的 Spring Data示例库 中找到一个使用BOM的工作实例。有了这些,你可以在 <dependencies /> 块中声明你想使用的没有版本的Spring Data模块,如下所示。
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>
2.1. 使用Spring Boot的依赖管理
Spring Boot会为你选择一个最新版本的Spring Data模块。如果你仍然想升级到较新的版本,将 spring-data-releasetrain.version 属性设置为你想使用的train version 和 iteration。
3. 对象映射的基础知识
本节涵盖了Spring Data对象映射、对象创建、字段和属性访问、可变性和不可变性的基本原理。注意,本节只适用于不使用底层数据存储的对象映射的Spring Data模块(如JPA)。此外,请务必查阅特定于存储的对象映射部分,如索引、自定义列或字段名或类似内容。
Spring Data对象映射的核心职责是创建domain对象的实例,并将存储的本地数据结构映射到这些对象上。这意味着我们需要两个基本步骤。
-
通过使用暴露的构造函数之一来创建实例。
-
Instance population to materialize all exposed properties.
3.1. Object 创建
Spring Data会自动尝试检测一个持久化实体的构造函数,以用于将该类型的对象具体化。该解析算法的工作原理如下。
-
如果有一个用
@PersistenceCreator注解的静态工厂方法,那么就使用它。 -
如果有一个单一的构造函数,它就被使用。
-
如果有多个构造函数,并且正好有一个被注解为
@PersistenceCreator,那么就使用它。 -
如果类型是Java
Record,则使用规范的构造函数。 -
如果有一个无参数的构造函数,它将被使用。其他构造函数将被忽略。
值解析假定构造器/工厂方法参数名与实体的属性名相匹配,即解析将在属性被填充的情况下进行,包括映射中的所有定制(不同的数据存储列或字段名等)。这也需要在class文件中提供参数名称信息,或者在构造函数上提供 @ConstructorProperties 注解。
通过使用Spring Framework的 @Value 值注解,可以使用store特定的SpEL表达式来定制值解析。请查阅关于 store 特定映射的章节以了解更多细节。
3.2. 属性填充
一旦实体的实例被创建,Spring Data就会填充该类的所有剩余持久化属性。除非已经由实体的构造函数填充(即通过其构造函数参数列表设置),否则 identifier 属性将首先被填充,以允许解决循环对象引用。之后,所有尚未被构造函数填充的非瞬时(non-transient)属性都被设置在实体实例上。为此,我们使用以下算法。
-
如果该属性是不可变的,但暴露了一个
with…方法(见下文),我们使用with…方法来创建一个具有新属性值的新实体实例。 -
如果定义了属性 access(即通过getter和setter访问),我们就调用setter方法。
-
如果该属性是可变的,我们直接设置该字段。
-
如果属性是不可变的,我们就使用持久化操作(见 Object 创建)所使用的构造函数来创建一个实例的副本。
-
默认情况下,我们直接设置字段的值。
让我们来看看以下实体。
class Person {
private final @Id Long id; (1)
private final String firstname, lastname; (2)
private final LocalDate birthday;
private final int age; (3)
private String comment; (4)
private @AccessType(Type.PROPERTY) String remarks; (5)
static Person of(String firstname, String lastname, LocalDate birthday) { (6)
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}
Person withId(Long id) { (1)
return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
}
void setRemarks(String remarks) { (5)
this.remarks = remarks;
}
}
| 1 | id(identifier)属性是 final 的,但在构造函数中设置为 null。该类暴露了一个 withId(…) 方法,用于设置id,例如,当一个实例被插入到数据存储中并且已经生成了一个id。当一个新的 Person 实例被创建时,原来的 Person 实例保持不变。同样的模式通常适用于其他的属性,这些属性是存储管理的,但可能要为持久化操作而改变。with 方法是可选的,因为持久化构造函数(见6)实际上是一个复制构造函数,设置该属性将被转化为创建一个新的实例,并应用新的id值。 |
| 2 | firstname 和 lastname 属性是普通的不可变的属性,可能通过 getter 暴露。 |
| 3 | age 属性是一个不可变的,但是从 birthday 属性派生出来的。在所示的设计中,数据库的值将胜过默认值,因为Spring Data使用唯一声明的构造函数。即使意图是优先考虑计算,重要的是这个构造函数也将 age 作为参数(有可能忽略它),否则属性填充步骤将试图设置 age 字段,但由于它是不可变的,而且没有 with… 方法存在,因此失败了。 |
| 4 | comment 属性是可变的,通过直接设置其字段来填充。 |
| 5 | remarks 属性是可变的,通过调用setter方法来填充。 |
| 6 | 该类暴露了一个工厂方法和一个用于创建对象的构造器。这里的核心思想是使用工厂方法而不是额外的构造函数,以避免通过 @PersistenceCreator 进行构造函数消歧义的需要。相反,属性的缺省是在工厂方法中处理的。如果你想让Spring Data使用工厂方法进行对象实例化,请用 @PersistenceCreator 来注解它。 |
3.3. 一般建议
-
尽量坚持使用不可变的对象 --不可变的对象创建起来很简单,因为具体化一个对象只需要调用其构造函数即可。同时,这也避免了你的domain对象充满了允许客户端代码操纵对象状态的 setter 方法。如果你需要这些,最好使它们受到
package的保护,这样它们就只能被有限的共存类型所调用。纯构造函数实例化属性比填充快30%。 -
提供一个全参数构造函数 — 即使你不能或不想将你的实体建模为不可变的值,提供一个将实体的所有属性作为参数的构造函数仍有价值,包括可变的属性,因为这允许对象映射跳过属性填充以获得最佳性能。
-
使用工厂方法而不是重载构造函数,以避免
@PersistenceCreator— 由于需要全参数构造函数以获得最佳性能,我们通常希望暴露更多的应用用例特定的构造函数,省略自动生成的ID等东西。宁愿使用静态工厂方法来暴露这些全参数构造函数的变体,这是一种既定的模式。 -
确保你遵守允许生成的实例化器(instantiator)和属性访问器(accessor)类被使用的约束。 —
-
对于要生成的ID,仍然使用 final 字段与全参数持久化构造函数(首选)或
with…方法相结合 — -
使用Lombok来避免模板代码 --由于持久化操作通常需要一个接受所有参数的构造器,它们的声明变成了繁琐的重复的模板参数到字段的分配,通过使用 Lombok 的
@AllArgsConstructor可以最好地避免。
3.3.1. 属性覆盖
Java允许对 domain 类进行灵活的设计,子类可以定义一个在其父类中已经用相同名称声明的属性。考虑一下下面的例子。
public class SuperType {
private CharSequence field;
public SuperType(CharSequence field) {
this.field = field;
}
public CharSequence getField() {
return this.field;
}
public void setField(CharSequence field) {
this.field = field;
}
}
public class SubType extends SuperType {
private String field;
public SubType(String field) {
super(field);
this.field = field;
}
@Override
public String getField() {
return this.field;
}
public void setField(String field) {
this.field = field;
// optional
super.setField(field);
}
}
这两个类都使用可分配类型来定义一个 field。然而,SubType 会影射 SuperType.field。根据类的设计,使用构造函数可能是设置 SuperType.field 的唯一默认方法。另外,在 setter 中调用 super.setField(…) 可以在 SuperType 中设置 field。所有这些机制在某种程度上都会产生冲突,因为这些属性共享相同的名称,但可能代表两个不同的值。如果类型不可分配,Spring Data会跳过父类属性。也就是说,被覆盖的属性的类型必须可分配给它的父类的属性类型才能被注册为覆盖(override),否则父类的属性就会被认为是 transient 的。我们一般建议使用不同的属性名称。
Spring Data模块通常支持持有不同 value 的重写属性。从编程模型的角度来看,有几件事需要考虑。
-
哪些属性应该被持久化(默认为所有声明的属性)?你可以通过用
@Transient来注解这些属性来排除它们。 -
如何在你的数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,所以你应该至少使用一个明确的字段/列名来注解其中的一个属性。
-
不能使用
@AccessType(PROPERTY),因为在不对 setter 实现做任何进一步假设的情况下,一般不能设置父类属性。
3.4. 对 Kotlin 的支持
Spring Data 适应了 Kotlin 的具体特性,允许对象的创建和变异(mutation)。
3.4.1. Kotlin object 创建
Kotlin类支持实例化,所有的类默认是不可变的,需要明确的属性声明来定义可变的属性。
Spring Data会自动尝试检测一个持久化实体的构造函数,以用于将该类型的对象具体化。 该解析算法的工作原理如下。
-
如果有一个构造函数被注解为
@PersistenceCreator,它将被使用。 -
如果类型是 Kotlin data cass,则使用 primary 构造函数。
-
如果有一个用
@PersistenceCreator注解的静态工厂方法,那么就使用它。 -
如果有一个单一的构造函数,它就被使用。
-
如果有多个构造函数,并且正好有一个被
@PersistenceCreator注解,那么它就被使用。 -
如果该类型是一个Java Record ,则使用规范的构造函数。
-
如果有一个无参数的构造函数,它将被使用。其他构造函数将被忽略。
考虑以下 data 类 Person。
data class Person(val id: String, val name: String)
上面的类编译成一个典型的具有显式构造函数的类。我们可以通过添加另一个构造函数来定制这个类,并用 @PersistenceCreator 来注释它,以表明构造函数的偏好。
data class Person(var id: String, val name: String) {
@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}
Kotlin通过允许在未提供参数时使用默认值来支持参数的可选性。当Spring Data检测到具有参数默认值的构造函数时,如果数据存储没有提供值(或简单地返回 null),那么它就会不提供这些参数,这样Kotlin就可以应用参数默认值。考虑以下类,它对 name 应用参数默认值。
data class Person(var id: String, val name: String = "unknown")
每当 name 参数不是结果的一部分或者其值为 null 时,那么 name 就默认为 unknown。
3.4.2. Kotlin data class 的属性填充
在Kotlin中,所有的类默认都是不可变的,需要明确的属性声明来定义可变属性。考虑下面这个 data class Person。
data class Person(val id: String, val name: String)
这个类实际上是不可变的。它允许创建新的实例,因为 Kotlin 生成了一个 copy(…) 方法,该方法创建了新的对象实例,复制了现有对象的所有属性值并应用了作为参数提供给该方法的属性值。
3.4.3. Kotlin 属性覆盖
Kotlin允许声明 属性覆盖 来改变子类中的属性。
open class SuperType(open var field: Int)
class SubType(override var field: Int = 1) :
SuperType(field) {
}
这样的安排渲染了两个名为 field 的属性。Kotlin为每个类中的每个属性生成了属性访问器(getter 和 setter),代码看起来像如下。
public class SuperType {
private int field;
public SuperType(int field) {
this.field = field;
}
public int getField() {
return this.field;
}
public void setField(int field) {
this.field = field;
}
}
public final class SubType extends SuperType {
private int field;
public SubType(int field) {
super(field);
this.field = field;
}
public int getField() {
return this.field;
}
public void setField(int field) {
this.field = field;
}
}
SubType 上的Getter和Setter只设置 SubType.field 而不是 SuperType.field。在这样的安排中,使用构造函数是设置 SuperType.field 的唯一默认方法。通过 this.SuperType.field = … 给 SubType 添加一个方法来设置 SuperType.field 是可能的,但是超出了支持的惯例。属性覆盖在某种程度上会产生冲突,因为这些属性共享相同的名称,但可能代表两个不同的值。我们通常建议使用不同的属性名称。
Spring Data模块通常支持持有不同value的重写属性。从编程模型的角度来看,有几件事需要考虑。
-
哪些属性应该被持久化(默认为所有声明的属性)?你可以通过用
@Transient来注解这些属性来排除它们。 -
如何在你的数据存储中表示属性?对不同的值使用相同的字段/列名通常会导致数据损坏,所以你应该至少使用一个明确的字段/列名来注解其中的一个属性。
-
@AccessType(PROPERTY)不能使用,因为不能设置父类属性。
4. 与 Spring Data Repository 一起工作
Spring Data Repository 抽象的目标是大大减少为各种持久性store实现数据访问层所需的模板代码量。
|
Spring Data Repository 文档和你的模块 本章解释了Spring Data Repository 的核心概念和接口。本章的信息是从Spring Data Commons模块中提取的。它使用了Jakarta Persistence API(JPA)模块的配置和代码样本。 如果你想使用XML配置,你应该将XML命名空间声明和要扩展的类型调整为你使用的特定模块的等价物。“命名空间参考” 涵盖了XML配置,所有支持 Repository API的Spring Data模块都支持这种配置。 “Repository query 关键字” 涵盖了 Repository 抽象所支持的一般的查询方法关键字。关于你的模块的具体功能的详细信息,请参阅本文档中关于该模块的章节。 |
4.1. 核心概念
Spring Data repository 抽象的中心接口是 Repository。它把要管理的 domain 类以及 domain 类的ID类型作为泛型参数。这个接口主要是作为一个标记接口,用来捕捉工作中的类型,并帮助你发现扩展这个接口的接口。 CrudRepository 和 ListCrudRepository 接口为被管理的实体类提供复杂的CRUD功能。
CrudRepository 接口public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity); (1)
Optional<T> findById(ID primaryKey); (2)
Iterable<T> findAll(); (3)
long count(); (4)
void delete(T entity); (5)
boolean existsById(ID primaryKey); (6)
// … more functionality omitted.
}
| 1 | 保存给定的实体。 |
| 2 | 根据ID返回实体。 |
| 3 | 返回所有实体。 |
| 4 | 返回实体数量。 |
| 5 | 删除给定的实体。 |
| 6 | 根据ID判断实体是否存在。 |
ListCrudRepository 提供了同等的方法,但它们返回 List,而 CrudRepository 的方法返回 Iterable。
我们还提供了持久化技术的特定抽象,如 JpaRepository 或 MongoRepository。这些接口扩展了 CrudRepository,除了像 CrudRepository 这样相当通用的持久化技术的接口之外,还暴露了底层持久化技术的能力。
|
除了 CrudRepository 之外,还有一个 PagingAndSortingRepository 的抽象,它增加了额外的分页,排序方法。
PagingAndSortingRepository 接口public interface PagingAndSortingRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
例如,访问第2页的 User ,每页20条数据,你可以这样:
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));
除了 query 方法外,count 和 delete 查询的查询派生也是可用的。下面的列表显示了派生的 count 查询的接口定义。
interface UserRepository extends CrudRepository<User, Long> {
long countByLastname(String lastname);
}
下面的列表显示了一个派生的 delete 查询的接口定义。
interface UserRepository extends CrudRepository<User, Long> {
long deleteByLastname(String lastname);
List<User> removeByLastname(String lastname);
}
4.2. Query 方法
标准的CRUD Repository 通常有对底层数据store的查询。使用Spring Data,声明这些查询成为一个四步过程。
-
声明一个扩展
Repository或其子接口之一的接口,并将其泛型指定为它应该处理的domain类和ID类型,如以下例子所示。interface PersonRepository extends Repository<Person, Long> { … } -
在接口中声明query方法。
interface PersonRepository extends Repository<Person, Long> { List<Person> findByLastname(String lastname); } -
设置Spring为这些接口创建代理实例,可以用JavaConfig或XML 配置。
Java@EnableJpaRepositories class Config { … }XML<?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:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> <repositories base-package="com.acme.repositories"/> </beans>本例中使用的是JPA namespace。如果你对任何其他store使用 Repository 抽象,你需要把它改为你的store模块的适当 namespace 声明。换句话说,你应该把
jpa换成,例如mongodb。请注意,JavaConfig 并没有明确地配置
package,因为被注解的类的package是默认使用的。要自定义要扫描的包,请使用数据store特定库的@EnableJpaRepositories注解的basePackage…属性之一。class SomeClient { private final PersonRepository repository; SomeClient(PersonRepository repository) { this.repository = repository; } void doSomething() { List<Person> persons = repository.findByLastname("Matthews"); } }
下面的章节将详细解释每一个步骤。
4.3. 定义 Repository 接口
要定义一个 repository 接口,你首先需要定义一个domain类专用的 repository 接口。该接口必须继承 Repository,并将其泛型设置为domain类和ID类。如果你想为该domain类公开CRUD方法,你可以继承 CrudRepository,或其变体,而不是 Repository。
4.3.1. 稍微修改 Repository 的定义
有几种变体可以让你开始使用你的 repository 接口。
典型的方法是继承 CrudRepository,它为你提供了 CRUD 功能的方法。CRUD是指创建、读取、更新、删除。在3.0版本中,我们还引入了 ListCrudRepository,它与 CrudRepository 非常相似,但对于那些返回多个实体的方法,它返回一个 List 而不是一个 Iterable,你可能会发现它更容易使用。
如果你使用的是响应式store,你可以选择 ReactiveCrudRepository,或者 RxJava3CrudRepository,这取决于你使用的是哪种响应式框架。
如果你使用的是Kotlin,你可以选择 CoroutineCrudRepository,它利用了Kotlin的 coroutine(协程)。
额外的你可以扩展 PagingAndSortingRepository、ReactiveSortingRepository、RxJava3SortingRepository 或 CoroutineSortingRepository,如果你需要允许指定一个 Sort 抽象的方法,或者在第一种情况下是 Pageable 抽象。请注意,各种排序 repository 不再像Spring Data 3.0之前的版本那样扩展各自的CRUD库。因此,如果你想获得这两个接口的功能,你需要扩展这两个接口。
如果你不想扩展Spring Data接口,你也可以用 @RepositoryDefinition 来注解你的 repository 接口。扩展CRUD repository 接口之一会暴露出一套完整的方法来操作你的实体。如果你想对暴露的方法有所选择,可以从CRUD repository 复制你想暴露的方法到你的 domain repository。这样做时,你可以改变方法的返回类型。如果可能的话,Spring Data会尊重返回类型。例如,对于返回多个实体的方法,你可以选择 Iterable<T>、List<T>、Collection<T> 或 VAVR list
如果你的应用程序中的许多 repository 应该有相同的方法集,你可以定义你自己的基础接口来继承。这样的接口必须用 @NoRepositoryBean 来注释。这可以防止Spring Data试图直接创建它的实例而导致异常,因为它仍然包含一个泛型变量,Spring data 无法确定该 repository 的实体。
下面的例子展示了如何有选择地公开CRUD方法(本例中为 findById 和 save)。
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {
Optional<T> findById(ID id);
<S extends T> S save(S entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}
在前面的例子中,你为所有的 domain repository 定义了一个通用的基础接口,并暴露了 findById(…) 以及 save(…)。这些方法被路由到Spring Data提供的你所选择的store的基础 repository 实现(例如,如果你使用JPA,实现是 SimpleJpaRepository),因为它们与 CrudRepository 中的方法签名一致。
所以 UserRepository 现在可以保存用户,通过ID查找单个用户,通过电子邮件地址查找 User。
中间的 repository 接口被注解为 @NoRepositoryBean。确保你在所有Spring Data不应该在运行时创建实例的 repository 接口上添加该注解。
|
4.3.2. 在多个Spring数据模块中使用 Repository
在你的应用程序中使用一个独特的Spring Data模块使事情变得简单,因为定义范围内的所有 repository 接口都绑定到Spring Data模块。有时,应用程序需要使用一个以上的Spring Data模块。在这种情况下,repository 定义必须区分持久化技术。当它检测到类路径上有多个 repository 工厂时,Spring Data会进入严格的 repository 配置模式。严格的配置使用 repository 或domain类的细节来决定 repository 定义的Spring Data模块绑定。
-
如果 repository 定义 继承了特定模块的 repository,那么它就是特定Spring Data模块的有效候选 repository。
-
如果 domain 类被注解了 模块特定的类型注解,它就是特定Spring Data模块的有效候选者。Spring Data模块接受第三方注解(如JPA的
@Entity)或提供自己的注解(如Spring Data MongoDB和Spring Data Elasticsearch的@Document)。
下面的例子显示了一个使用特定模块接口的 repository(本例中为JPA)。
interface MyRepository extends JpaRepository<User, Long> { }
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }
interface UserRepository extends MyBaseRepository<User, Long> { … }
MyRepository 和 UserRepository 在其类型层次上扩展了 JpaRepository。它们是Spring Data JPA 模块的有效候选者。
下面的例子显示了一个使用通用(泛型)接口的 repository。
interface AmbiguousRepository extends Repository<User, Long> { … }
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }
interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }
AmbiguousRepository 和 AmbiguousUserRepository 在其类型层次结构中只继承了 Repository 和 CrudRepository 。虽然在使用唯一的Spring Data模块时这很好,但多个模块无法区分这些 repository 应该被绑定到哪个特定的Spring Data。
下面的例子显示了一个使用带注解的domain类的repository。
interface PersonRepository extends Repository<Person, Long> { … }
@Entity
class Person { … }
interface UserRepository extends Repository<User, Long> { … }
@Document
class User { … }
PersonRepository 引用了 Person,它被 JPA 的 @Entity 注解所注解,所以这个 repository 显然属于Spring Data JPA。UserRepository 引用了 User,它被Spring Data MongoDB 的 @Document 注解所注解。
下面的坏例子显示了一个使用混合注解的 domain 类的 Repository。
interface JpaPersonRepository extends Repository<Person, Long> { … }
interface MongoDBPersonRepository extends Repository<Person, Long> { … }
@Entity
@Document
class Person { … }
这个例子展示了一个同时使用JPA和Spring Data MongoDB注解的 domain 类。它定义了两个repository:JpaPersonRepository 和 MongoDBPersonRepository。一个用于JPA,另一个用于MongoDB的使用。Spring Data不再能够区分这些repository,这导致了未定义的行为。
Repository 类型细节和区分domain类注解用于严格的repository库配置,以确定特定Spring Data模块的repository候选者。在同一domain类型上使用多个持久化技术的特定注解是可能的,并且能够在多个持久化技术中重复使用domain类型。然而,Spring Data就不能再确定一个唯一的模块来绑定repository了。
区分 repository 的最后一个方法是通过对 repository base package的扫描。base package 定义了扫描 repository 接口定义的起点,这意味着将 repository 的定义放在适当的包中。默认情况下,注解驱动的配置使用配置类所在的base package。基于XML的配置中的base package,需要手动强制配置。
下面的例子显示了注解驱动的 base package 的配置。
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
4.4. 定义 Query 方法
repository 代理有两种方法可以从方法名中推导出 repository 特定的查询。
-
通过直接从方法名派生出查询。
-
通过使用手动定义的查询。
可用的选项取决于实际的store。然而,必须有一个策略来决定创建什么样的实际查询。下一节将介绍可用的选项。
4.4.1. Query 的查询策略
下列策略可用于 repository 基础设施解析查询。
对于 XML 配置,你可以通过 query-lookup-strategy 属性在命名空间配置策略。
对于 Java 配置,你可以使用 EnableJpaRepositories 注解的 queryLookupStrategy 属性。有些策略可能不支持特定的datastore。
-
CREATE试图从查询方法名称中构建一个特定的存储查询。一般的做法是从方法名中删除一组已知的前缀,然后解析方法的其余部分。你可以在 “Query 创建” 中阅读更多关于查询构建的信息。USE_DECLARED_QUERY试图找到一个已声明的查询,如果找不到就会抛出一个异常。查询可以由某处的注解来定义,也可以通过其他方式来声明。请参阅特定store的文档以找到该store的可用选项。如果 repository 基础设施在启动时没有为该方法找到一个已声明的查询,则会失败。 -
CREATE_IF_NOT_FOUND(默认) 结合了CREATE和USE_DECLARED_QUERY。它首先查找一个已声明的查询, 如果没有找到已声明的查询, 它将创建一个基于方法名的自定义查询。这是默认的查询策略,因此,如果你没有明确地配置任何东西,就会使用这种策略。它允许通过方法名快速定义查询,但也可以根据需要通过引入已声明的查询对这些查询进行自定义调整。
4.4.2. Query 创建
内置在Spring Data repository 基础架构中的查询 builder 机制对于在资源库的实体上建立约束性查询非常有用。
下面的例子展示了如何创建一些查询。
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
解析查询方法名称分为主语和谓语。第一部分(find…By, exists…By)定义了查询的主语,第二部分形成谓语。引入句(主语)可以包含进一步的表达。在 find(或其他引入关键词)和 By 之间的任何文本都被认为是描述性的,除非使用一个限制结果的关键词,如 Distinct 在要创建的查询上设置一个不同的标志,或 Top / First 来限制查询结果。
附录中包含了 查询方法主语关键词 和 查询方法谓语关键词的完整列表,包括排序和字母修饰语。然而,第一个 By 作为分界符,表示实际条件谓词的开始。在一个非常基本的层面上,你可以在实体属性上定义条件,并用 And 和 Or 来连接它们。
解析方法的实际结果取决于你为之创建查询的持久性store。然而,有一些东西需要注意。
-
表达式通常是属性遍历与可以串联的运算符的组合。你可以用
AND和OR来组合属性表达式。你还可以得到对属性表达式的运算符的支持,如Between,LessThan,GreaterThan, 和Like。支持的运算符可能因 datastore 的不同而不同,所以请查阅参考文档的适当部分。 -
方法解析器支持为单个属性(例如,
findByLastnameIgnoreCase(…))或支持忽略大小写的类型的所有属性(通常是字符串实例—例如,findByLastnameAndFirstnameAllIgnoreCase(…))设置忽略大小写标志。是否支持忽略大小写可能因store而异,所以请查阅参考文档中的相关章节,了解特定store的查询方法。 -
你可以通过在引用属性的查询方法中附加一个
OrderBy子句,并提供一个排序方向(Asc或Desc)来应用静态排序。要创建一个支持动态排序的查询方法,请参阅 “特殊参数处理”。
4.4.3. 属性表达式
属性表达式只能引用被管理实体的一个直接属性,如前面的例子所示。在查询创建时,你已经确保解析的属性是被管理的domian类的一个属性。然而,你也可以通过遍历嵌套属性来定义约束。考虑一下下面的方法签名。
List<Person> findByAddressZipCode(ZipCode zipCode);
假设 Person 有一个带有 ZipCode 的 Address。在这种情况下,该方法创建 x.address.zipCode 属性遍历。解析算法首先将整个部分(AddressZipCode)解释为属性,并检查domain类中是否有该名称的属性(未加首字母)。如果算法成功,它就使用该属性。如果没有,该算法将源头的驼峰字母部分从右侧分割成一个头和一个尾,并试图找到相应的属性—在我们的例子中,是 AddressZip 和 Code。如果该算法找到了具有该头部的属性,它就取其尾部,并从那里继续向下构建树,以刚才描述的方式将尾部分割开来。如果第一次分割不匹配,该算法将分割点移到左边(Address, ZipCode)并继续。
虽然这在大多数情况下应该是有效的,但该算法有可能选择错误的属性。假设 Person 类也有一个 addressZip 属性。该算法将在第一轮分割中已经匹配,选择错误的属性,并且失败(因为 addressZip 的类型可能没有 code 属性)。
为了解决这个模糊的问题,你可以在你的方法名里面使用 _ 来手动定义遍历点。因此,我们的方法名称将如下。
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因为我们把下划线字符当作一个保留字符,所以我们强烈建议遵循标准的Java命名惯例(也就是说,不要在属性名中使用下划线,而要使用驼峰大写)。
4.4.4. 特殊参数处理
为了处理你的查询中的参数,定义方法参数,正如在前面的例子中已经看到的。除此之外,基础设施还能识别某些特定的类型,如 Pageable 和 Sort,以动态地将分页和排序应用于你的查询。下面的例子演示了这些功能。
Pageable、Slice 和 Sort。Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
API中定义的 Sort 和 Pageable 实际调用时不能为 null。如果你不想应用任何排序或分页,请使用 Sort.unsorted() 和 Pageable.unpaged()。
|
第一个方法让你把 org.springframework.data.domain.Pageable 实例传递给 query 方法,以动态地将分页添加到你静态定义的查询中。一个 Page 知道可用的元素和页面的总数。它是通过基础设施触发一个 count 查询来计算总数量。由于这可能是昂贵的(取决于使用的store),你可以返回一个 Slice。一个 Slice 只知道下一个 Slice 是否可用,当遍历一个较大的结果集时,这可能就足够了。
排序选项也是通过 Pageable 实例处理的。如果你只需要排序,在你的方法中加入 org.springframework.data.domain.Sort 参数。正如你所看到的,返回一个 List 也是可能的。在这种情况下,构建实际的 Page 实例所需的额外元数据并没有被创建(这反过来意味着不需要发出额外的 count 查询)。相反,它限制了查询,只查询给定范围的实体。
| 要想知道你 query 的总页数,你必须触发一个额外的count查询。默认情况下,这个查询是由你实际触发的查询派生出来的。 |
分页和排序
你可以通过使用属性名称来定义简单的排序表达式。你可以将表达式连接起来,将多个 criteria 收集到一个表达式中。
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
对于定义排序表达式的更加类型安全的方式,从定义排序表达式的类型开始,使用方法引用来定义排序的属性。
TypedSort<Person> person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
TypedSort.by(…) 通过(通常)使用 CGlib 来使用运行时代理,这在使用 Graal VM Native 等工具时可能会干扰原生镜像的编译。
|
如果你的 store 实现支持 Querydsl,你也可以使用生成的 metamodel 类型来定义排序表达式。
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
4.4.5. 限制查询结果
你可以通过使用 first 或 top 关键字来限制查询方法的结果,这两个关键字可以互换使用。你可以在 top 或 first 后面附加一个可选的数值,以指定要返回的最大结果大小。如果不加数字,就会假定结果大小为 1。下面的例子显示了如何限制查询的大小。
Top 和 First 限制查询结果集User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
对于支持不同查询的数据集,限制表达式也支持 Distinct 关键字。另外,对于将结果集限制在一个实例的查询,支持用 Optional 关键字将结果包入。
如果分页或 slice 应用于 limit 查询的分页(以及可用页数的计算),则会在 limit 结果中应用。
通过使用 Sort 参数将结果与动态排序相结合,可以让你表达对 "K" 最小元素和 "K" 最大元素的查询方法。
|
4.4.6. Repository 方法返回 Collection 或 Iterable
返回多个结果的查询方法可以使用标准的Java Iterable、List 和 Set。除此之外,我们还支持返回Spring Data的 Streamable,这是 Iterable 的一个自定义扩展,以及 Vavr 提供的 collection 类型。请参考附录中对所有可能的 查询方法返回类型的解释。
使用 Streamable 作为 Query 方法的返回类型
你可以用 Streamable 来替代 Iterable 或任何 collection 类型。它提供了方便的方法来访问一个非并行的 Stream(Iterable 所没有的),并且能够在元素上直接 …filter(…) 和 …map(…),并将 Streamable 与其他元素连接起来。
interface PersonRepository extends Repository<Person, Long> {
Streamable<Person> findByFirstnameContaining(String firstname);
Streamable<Person> findByLastnameContaining(String lastname);
}
Streamable<Person> result = repository.findByFirstnameContaining("av")
.and(repository.findByLastnameContaining("ea"));
返回自定义的 Streamable Wrapper 类型
为集合提供专用的 wrapper 类型是一种常用的模式,为返回多个元素的查询结果提供API。通常,这些类型的使用是通过调用返回类似集合类型的 repository 方法,并手动创建一个 wrapper 类型的实例。你可以避免这个额外的步骤,因为Spring Data允许你使用这些 wrapper 类型作为查询方法的返回类型,如果它们满足以下条件。
-
该类型实现了
Streamable. -
该类型暴露了一个构造函数或一个名为
of(…)或valueOf(…)的静态工厂方法,该方法将Streamable作为一个参数。
下面列出了一个例子。
class Product { (1)
MonetaryAmount getPrice() { … }
}
@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)
private final Streamable<Product> streamable;
public MonetaryAmount getTotal() { (3)
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
@Override
public Iterator<Product> iterator() { (4)
return streamable.iterator();
}
}
interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text); (5)
}
| 1 | 一个 Product 实体,公开API以访问 product 的price。 |
| 2 | 一个 Streamable<Product> 的封装类型,可以通过使用 Products.of(…)(用Lombok注解创建的工厂方法)来构建。使用 Streamable<Product> 的标准构造函数也可以。 |
| 3 | wrapper 类型暴露了一个额外的API,在 Streamable<Product> 上计算新值。 |
| 4 | 实现 Streamable 接口并委托给实际结果。 |
| 5 | wrapper 类型 Products 可以直接作为查询方法的返回类型。你不需要返回 Streamable<Product> 并在调用 repository 后手动封装它。 |
支持 Vavr 集合
Vavr 是一个拥抱Java中函数式编程概念的库。它带有一组自定义的集合类型,你可以将其作为查询方法的返回类型,如下表所示。
| Vavr collection 类型 | 使用的Vavr实现类型 | 有效的Java原类型 |
|---|---|---|
|
|
|
|
|
|
|
|
|
你可以使用第一列中的类型(或其子类型)作为查询方法的返回类型,并根据实际查询结果的Java类型(第三列),获得第二列中的类型作为实现类型使用。或者,你可以声明 Traversable(相当于Vavr Iterable),然后我们从实际返回值中派生出实现类。也就是说,java.util.List 会变成 Vavr List 或 Seq,java.util.Set 会变成 Vavr LinkedHashSet Set,以此类推。
4.4.7. Repository 方法的 Null 处理
从Spring Data 2.0开始,返回单个聚合实例的 repository CRUD方法使用Java 8的 Optional 来表示可能没有的值。除此之外,Spring Data还支持在查询方法上返回以下 wrapper 类型。
-
com.google.common.base.Optional -
scala.Option -
io.vavr.control.Option
另外,查询方法可以选择完全不使用 wrapper 类型。没有查询结果的话会通过返回 null 来表示。Repository 方法返回集合、集合替代物、wrapper和流时,保证不会返回 null,而是返回相应的空(Empty)表示。详见 “Repository 查询返回类型”。
null约束注解
你可以通过使用 Spring Framework的nullability注解 来表达 repository 方法的 nullability 约束。它们提供了一种友好的方法,并在运行时选择加入 null 值检查,如下所示。
-
@NonNullApi: 在 package 的层面上用于声明参数和返回值的默认行为,分别是既不接受也不产生null值。 -
@NonNull: 用于不得为null的参数或返回值(不需要在参数和返回值中使用@NonNullApi)。 -
@Nullable: 用于可以是null的参数或返回值。
Spring注解是用 JSR 305 注解(一个休眠状态但广泛使用的JSR)进行元注解的。JSR 305元注解让工具供应商(如 IDEA、 Eclipse 和 Kotlin)以通用的方式提供 null-safety 支持,而不需要对Spring注解进行硬编码支持。为了在运行时检查查询方法的无效性约束,你需要通过在 package-info.java 中使用 Spring 的 @NonNullApi,在包级别上激活null约束,如下例所示。
package-info.java@org.springframework.lang.NonNullApi
package com.acme;
一旦定义了非null约束,repository 的查询方法调用就会在运行时被验证是否有nul约束。如果查询结果违反了定义的约束条件,就会抛出一个异常。这种情况发生在方法会返回 null,但被声明为non-nullable(在 repository 所在的包上定义注解的默认值)。如果你想再次选择加入允许结果为null,可以有选择地在个别方法上使用 @Nullable。使用本节开头提到的结果wrapper类型继续按预期工作:一个空的结果被翻译成代表不存在的值。
下面的例子显示了刚才描述的一些技术。
package com.acme; (1)
interface UserRepository extends Repository<User, Long> {
User getByEmailAddress(EmailAddress emailAddress); (2)
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress); (3)
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); (4)
}
| 1 | repository 在一个包(或子包)中,我们已经为其定义了非空的行为。 |
| 2 | 当查询没有产生结果时,抛出一个 EmptyResultDataAccessException。当交给该方法的 emailAddress 为 null 时,抛出一个 IllegalArgumentException。 |
| 3 | 当查询没有产生结果时返回 null。也接受 null 作为 emailAddress 的值。 |
| 4 | 当查询没有产生结果时,返回 Optional.empty()。当交给该方法的 emailAddress 为 null 时,抛出一个 IllegalArgumentException。 |
基于Kotlin的 Repository 中的 Nullability
Kotlin在语言中加入了 对无效性约束的定义。Kotlin代码编译为字节码,它不通过方法签名来表达无效性约束,而是通过编译后的元数据。请确保在你的项目中包含 kotlin-reflect JAR,以实现对Kotlin的nullability约束的内省。Spring Data Repository 使用语言机制来定义这些约束,以应用相同的运行时检查,如下所示。
interface UserRepository : Repository<User, String> {
fun findByUsername(username: String): User (1)
fun findByFirstname(firstname: String?): User? (2)
}
| 1 | 该方法将参数和结果都定义为不可为空(Kotlin默认)。Kotlin编译器会拒绝那些向方法传递 null 的方法调用。如果查询产生了一个空的结果,就会抛出一个 EmptyResultDataAccessException。 |
| 2 | 这个方法接受 null 作为 firstname 参数,如果查询没有产生结果,则返回 null。 |
4.4.8. 流式(Stream)查询结果
你可以通过使用Java 8 Stream<T> 作为返回类型来增量地处理查询方法的结果。如下面的例子所示,不把查询结果包裹在 Stream 中,而是使用 data store 的特定方法来执行流式处理。
Stream<T>@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();
Stream<User> readAllByFirstnameNotNull();
@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
一个 Stream 可能包裹了底层 data store 的特定资源,因此,在使用后必须关闭。你可以通过使用 close() 方法来手动关闭 Stream,或者使用Java 7 try-with-resources 块来关闭,如下面的例子中所示。
|
Stream<T> result in a try-with-resources blocktry (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
目前并非所有的Spring Data模块都支持 Stream<T> 作为返回类型。
|
4.4.9. 异步(Asynchronous)查询结果
你可以通过使用 Spring的异步方法运行能力 来异步运行 repository 查询。这意味着该方法在调用后立即返回,而实际的查询发生在一个已经提交给Spring TaskExecutor 的任务中。异步查询与响应式查询不同,不应混合使用。关于响应式支持的更多细节,请参见store的特定文档。下面的例子显示了一些异步查询的案例。
@Async
Future<User> findByFirstname(String firstname); (1)
@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
| 1 | 使用 java.util.concurrent.Future 作为返回类型。 |
| 2 | 使用 Java 8 java.util.concurrent.CompletableFuture 作为返回类型。 |
4.5. 创建 Repository 实例
本节介绍了如何为定义的 repository 接口创建实例和Bean定义。
4.5.1. Java 配置
在Java配置类上使用store特有的 @EnableJpaRepositories 注解来定义 repository 激活的配置。关于基于Java的Spring容器配置的介绍,请参见 Spring参考文档中的JavaConfig。
启用 Spring Data Repository 的示例配置类似于以下内容。
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
前面的例子使用了JPA特定的注解,你可以根据你实际使用的store模块来改变它。这同样适用于 EntityManagerFactory Bean的定义。请看涉及store特定配置的章节。
|
4.5.2. XML 配置
每个Spring Data模块都包括一个 repositories 元素,让你定义一个Spring为你扫描的 base package,如下例所示。
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="com.acme.repositories" />
</beans:beans>
在前面的例子中,Spring被指示扫描 com.acme.repositories 及其所有子包,以寻找扩展 Repository 或其子接口之一的接口。对于找到的每个接口,基础设施都会注册持久化技术专用的 FactoryBean,以创建适当的代理,处理查询方法的调用。每个Bean都被注册在一个从接口名称衍生出来的Bean名称下,所以 UserRepository 的接口将被注册在 userRepository 下。嵌套的存储库接口的Bean名是以其包裹的类型名称为前缀。base package 属性允许通配符,这样你就可以定义一个扫描包的模式。
4.5.3. 使用 Filter
默认情况下,基础架构会抓取每个扩展了位于配置的 base package 下的持久化技术特定的 Repository 子接口的接口,并为其创建一个Bean实例。然而,你可能想要更精细地控制哪些接口为其创建Bean实例。要做到这一点,可以在 Repository 声明中使用 filter 元素。其语义与Spring的组件过滤器中的元素完全等同。详情请见 Spring参考文档 中的这些元素。
例如,为了排除某些接口作为 Repository Bean 的实例化,你可以使用以下配置。
@Configuration
@EnableJpaRepositories(basePackages = "com.acme.repositories",
includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeRepository") },
excludeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeOtherRepository") })
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
<repositories base-package="com.acme.repositories">
<context:exclude-filter type="regex" expression=".*SomeRepository" />
<context:include-filter type="regex" expression=".*SomeOtherRepository" />
</repositories>
前面的例子排除了所有以 SomeRepository 结尾的接口被实例化,包括以 SomeOtherRepository 结尾的接口。
4.5.4. 单独使用
你也可以在Spring容器之外使用资源库基础设施—例如,在CDI环境中。你仍然需要在你的classpath中使用一些Spring库,但是,一般来说,你也可以通过编程来设置 Repository。Repository 支持的Spring Data模块都有一个特定于持久化技术的 RepositoryFactory,你可以使用,如下所示。
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
4.6. 自定义 Spring Data Repository 的实现
Spring Data提供了各种选项来创建查询方法,只需少量编码。但当这些选项不符合你的需求时,你也可以为 repository 方法提供你自己的自定义实现。本节介绍了如何做到这一点。
4.6.1. 自定义个别 Repository
要用自定义的功能来丰富 repository,你必须首先定义一个片段(fragment)接口和自定义功能的实现,如下所示。
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
与片段接口对应的类名中最重要的部分是 Impl 后缀。
|
实现本身并不依赖于Spring Data,它可以是一个普通的Spring Bean。因此,你可以使用标准的依赖注入行为来注入对其他Bean(如 JdbcTemplate)的引用,参与到各个切面,等等。
然后你可以让你的 repository 接口继承片段接口,如下所示。
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {
// Declare query methods here
}
用你的存储库接口继承片段接口,结合了CRUD和自定义功能,并使其对客户端可用。
Spring Data Repository 是通过使用形成 repository 组合的片段来实现的。片段是基础repository、功能方面(如 QueryDsl),以及自定义接口和它们的实现。每当你为你的repository接口添加一个接口,你就通过添加一个片段来增强组合。基础repository和repository方面的实现是由每个Spring Data模块提供的。
下面的例子显示了自定义接口和它们的实现。
interface HumanRepository {
void someHumanMethod(User user);
}
class HumanRepositoryImpl implements HumanRepository {
public void someHumanMethod(User user) {
// Your custom implementation
}
}
interface ContactRepository {
void someContactMethod(User user);
User anotherContactMethod(User user);
}
class ContactRepositoryImpl implements ContactRepository {
public void someContactMethod(User user) {
// Your custom implementation
}
public User anotherContactMethod(User user) {
// Your custom implementation
}
}
下面的例子显示了一个扩展了 CrudRepository 的自定义 repository 的接口。
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {
// Declare query methods here
}
Repository可以由多个自定义实现组成,这些自定义实现按其声明的顺序被导入。自定义实现的优先级高于基础实现和Repository 切面。这种排序可以让你覆盖基础Repository和切面的方法,并在两个片段贡献了相同的方法签名时解决歧义。Repository片段不限于在单一存储库接口中使用。多个Repository可以使用一个片段接口,让你在不同的 Repository 中重复使用自定义的内容。
下面的例子显示了一个 Repository 片段及其实现。
save(…)interface CustomizedSave<T> {
<S extends T> S save(S entity);
}
class CustomizedSaveImpl<T> implements CustomizedSave<T> {
public <S extends T> S save(S entity) {
// Your custom implementation
}
}
下面的例子显示了一个使用前述 repository 片段的 repository。
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}
interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
配置
repository基础设施试图通过扫描发现repository的包下面的类来自动检测自定义实现片段。这些类需要遵循后缀默认为 Impl 的命名惯例。
下面的例子显示了一个使用默认后缀的 repository 和一个为后缀设置了自定义值的 repository。
@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
<repositories base-package="com.acme.repository" />
<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" />
前面例子中的第一个配置试图查找一个叫做 com.acme.repository.CustomizedUserRepositoryImpl 的类,作为一个自定义的 repository 实现。第二个例子试图查找 com.acme.repository.CustomizedUserRepositoryMyPostfix。
消除歧义
如果在不同的包中发现了具有匹配类名的多个实现,Spring Data会使用Bean名称来确定使用哪一个。
考虑到前面显示的 CustomizedUserRepository 的以下两个自定义实现,第一个实现被使用。它的Bean名是 customedUserRepositoryImpl,与片段接口(CustomizedUserRepository)加后缀 Impl 的名字一致。
package com.acme.impl.one;
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
package com.acme.impl.two;
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
如果你用 @Component("specialCustom") 来注解 UserRepository 接口,那么 Bean 的名字加上 Impl 就与 com.acme.impl.two 中为 repository 实现定义的名字相匹配,并被用来代替第一个接口。
手动注入
如果你的自定义实现只使用基于注解的配置和自动注入,前面所示的方法很好用,因为它被当作任何其他Spring Bean。如果你的实现片段Bean需要特殊的注入,你可以根据前文所述的约定来声明Bean并为其命名。然后,基础设施通过名称来引用手动定义的Bean定义,而不是自己创建一个。下面的例子展示了如何手动注入一个自定义的实现。
class MyClass {
MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
…
}
}
<repositories base-package="com.acme.repository" />
<beans:bean id="userRepositoryImpl" class="…">
<!-- further configuration -->
</beans:bean>
4.6.2. 自定义 Base Repository
当你想定制基础 repository 的行为时,上一节描述的方法需要定制每个 repository 的接口,以便所有的 repository 都受到影响。为了改变所有 repository 的行为,你可以创建一个扩展持久化技术特定 repository 基类的实现。然后这个类作为 repository 代理的自定义基类,如下面的例子所示。
class MyRepositoryImpl<T, ID>
extends SimpleJpaRepository<T, ID> {
private final EntityManager entityManager;
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
该类需要有一个super类的构造器,store特定的 repository factory 实现使用该构造器。如果repository 基类有多个构造函数,请复写其中一个构造函数,该构造函数需要一个 EntityInformation 和一个store特定的基础设施对象(如 EntityManager 或模板类)。
|
最后一步是让Spring Data基础设施意识到定制的 repository base 类。在配置中,你可以通过使用 repositoryBaseClass 来做到这一点,如下面的例子所示。
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
<repositories base-package="com.acme.repository"
base-class="….MyRepositoryImpl" />
4.7. 从 Aggregate Root (聚合ROOT)中发布事件
由 Repository 管理的实体是 aggregate root。在领域驱动设计应用程序中,这些aggregate root通常会发布 domain 事件。Spring Data提供了一个名为 @DomainEvents 的注解,你可以在 aggregate root 的一个方法上使用该注解,以使这种发布尽可能地简单,如下例所示。
class AnAggregateRoot {
@DomainEvents (1)
Collection<Object> domainEvents() {
// … return events you want to get published here
}
@AfterDomainEventPublication (2)
void callbackMethod() {
// … potentially clean up domain events list
}
}
| 1 | 使用 @DomainEvents 的方法可以返回一个单一的事件实例或一个事件的集合。它必须不接受任何参数。 |
| 2 | 在所有的事件都被发布后,我们有一个用 @AfterDomainEventPublication 注解的方法。你可以用它来潜在地清理要发布的事件列表(除其他用途外)。 |
每次调用Spring Data Repository的 save(…)、saveAll(…)、delete(…) 或 deleteAll(…) 方法时都会调用这些方法。
4.8. Spring Data 扩展
本节记录了一组Spring Data扩展,这些扩展使Spring Data能够在各种情况下使用。目前,大部分的集成都是针对Spring MVC的。
4.8.1. Querydsl 扩展
Querydsl 是一个框架,可以通过其 fluent API构建静态类型的类似SQL的查询。
一些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 支持,在你的版本库接口上扩展 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);
4.8.2. Web 的支持
支持 repository 编程模型的Spring Data模块带有各种Web支持。web相关的组件需要添加 Spring MVC 到项目。其中一些甚至提供了与 Spring HATEOAS的整合。一般来说,集成支持是通过在你的 JavaConfig 配置类中使用 @EnableSpringDataWebSupport 注解来启用的,如下面例子所示。
@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 管理的 domain 类实例。 -
HandlerMethodArgumentResolver的实现,让Spring MVC从请求参数中解析Pageable和Sort实例。 -
Jackson模块 对
Point和Distance等类型进行序列化/反序列化,或存储特定的类型,这取决于使用的Spring数据模块。
使用 DomainClassConverter 类
DomainClassConverter 类让你在Spring MVC Controller 方法签名中直接使用 domain 类型,这样你就不需要通过 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先将路径变量转换为domain类的 id 类型来解决,最终通过调用为domain类注册的资源库实例 findById(…) 来访问该实例。
目前,repository 必须实现 CrudRepository 才有资格被发现进行转换。
|
使用 HandlerMethodArgumentResolvers 解析 Pageable 和 Sort
上一节 中的配置片段还注册了一个 PageableHandlerMethodArgumentResolver 以及一个 SortHandlerMethodArgumentResolver 的实例。注册后,Pageable 和 Sort 可以作为有效的controller方法参数,如下图所示。
@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 注解来定制它。
对 Pageable 的 Hypermedia 支持
Spring HATEOAS提供了一个表示 model 类(PagedResources),它允许用必要的页面元数据以及链接来丰富 Page 实例的内容,让客户轻松地浏览页面。Page 到 PagedResources 的转换是由Spring HATEOAS ResourceAssembler 接口的实现完成的,这个接口被称为 PagedResourcesAssembler。下面的例子展示了如何使用 PagedResourcesAssembler 作为 controller 方法的参数。
@Controller
class PersonController {
@Autowired PersonRepository repository;
@RequestMapping(value = "/persons", method = RequestMethod.GET)
HttpEntity<PagedResources<Person>> persons(Pageable pageable,
PagedResourcesAssembler assembler) {
Page<Person> persons = repository.findAll(pageable);
return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
}
}
启用配置,如前面的例子所示,让 PagedResourcesAssembler 被用作控制器方法的参数。对它调用 toResources(…) 有以下效果。
-
Page的内容成为PagedResources实例的内容。 -
PagedResources对象被附加了一个PageMetadata实例,它被填充了来自Page和基础PageRequest的信息。 -
PagedResources可能会有一个prev和一个next链接,这取决于页面的状态。这些链接指向该方法所映射的URI。添加到方法中的分页参数与PageableHandlerMethodArgumentResolver的设置相匹配,以确保链接可以在稍后被解析。
假设我们在数据库中有30个的 Person 实例。现在你可以触发一个请求(GET http://localhost:8080/persons),看到类似以下的输出。
{ "links" : [ { "rel" : "next",
"href" : "http://localhost:8080/persons?page=1&size=20" }
],
"content" : [
… // 20 Person instances rendered here
],
"pageMetadata" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
}
assembler 产生了正确的URI,并且还拾取了默认的配置,以便为即将到来的请求将参数解析为一个 Pageable。这意味着,如果你改变了配置,链接会自动遵守这一变化。默认情况下,assembler 会指向它被调用的controller方法,但你可以通过传递一个自定义的 Link 来定制,作为建立分页链接的基础,它重载了 PagedResourcesAssembler.toResource(..) 方法。
Spring Data Jackson 模块
核心模块和一些特定的存储模块与一组Jackson模块一起发布,用于Spring Data domain 域使用的类型,如 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。
以下domain类型的 Data binding mixins 由公共基础设施注册。
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 Databinding 的支持
你可以通过使用 JSONPath 表达式(需要 Jayway JsonPath)或 XPath 表达式(需要 XmlBeam)来使用 Spring Data 投影(在 投影 中描述)来绑定传入的请求的payload,如下例所示。
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
你可以将前面的例子中显示的类型作为Spring MVC controller 的方法参数,或者通过在 RestTemplate 的某个方法中使用 ParameterizedTypeReference。前面的方法声明将尝试在给定 document 中的任何地方找到 firstname。lastname 的XML查找是在传入 document 的顶层进行的。JSON的变体首先尝试顶层的 lastname,但是如果前者没有返回一个值,也会尝试嵌套在 user 子 document 中的 lastname。这样,源 document 结构的变化可以很容易地被减轻,而不需要客户端调用暴露的方法(通常是基于类的 payload 绑定的一个缺点)。
如 投影 中所述,支持嵌套投影。如果该方法返回一个复杂的、非接口类型,则使用Jackson ObjectMapper 来映射最终值。
对于Spring MVC,一旦 @EnableSpringDataWebSupport 被激活,并且classpath上有必要的依赖,必要的 converter 就会被自动注册。对于 RestTemplate 的使用,需要手动注册一个 ProjectingJackson2HttpMessageConverter(JSON)或 XmlBeamHttpMessageConverter。
欲了解更多信息,请参见 Spring Data 示例库 中的 web投影示例。
Querydsl 的 Web 支持
对于那些有 QueryDSL 集成的 store,你可以从 Request 查询字符串中包含的属性导出查询。
考虑下面这个查询字符串:
?firstname=Dave&lastname=Matthews
给出前面例子中的 User 对象,你可以通过使用 QuerydslPredicateArgumentResolver 将一个查询字符串解析为以下值,如下所示。
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
当在 classpath 上发现 Querydsl 时,该功能与 @EnableSpringDataWebSupport 一起被自动启用。
|
在方法签名中添加 @QuerydslPredicate 提供了一个随时可用的 Predicate,你可以通过使用 QuerydslPredicateExecutor 来运行它。
类型信息通常是由方法的返回类型来解决的。由于该信息不一定与 domain 类型相匹配,使用 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 的特定查找方法的访问。 |
| 2 | repository 接口上定义的 QuerydslBinderCustomizer 被自动拾取,并成为 @QuerydslPredicate(bindings=…) 的快捷方式。 |
| 3 | 定义 username 属性的绑定是一个简单的 contains 绑定。 |
| 4 | 定义 String 属性的默认绑定为不区分大小写的 contains 匹配。 |
| 5 | 将 password 属性排除在 Predicate 解析之外。 |
你可以在应用来自 repository 或 @QuerydslPredicate 的特定绑定之前,注册一个持有默认Querydsl绑定的 QuerydslBinderCustomizerDefaults bean。
|
4.8.3. Repository 填充
如果你使用Spring JDBC模块,你可能很熟悉对用SQL脚本填充 DataSource 的支持。在 repository 层面也有类似的抽象,尽管它不使用SQL作为数据定义语言,因为它必须是独立于store的。因此,填充器支持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来 unmarshall 对 repository 填充器的 marshall。
下面的例子显示了如何用 JAXB 来 unmarshall 一个 repository 填充器(populator)。
unmarshalling repository populator(使用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>
5. 投影
Spring Data的查询方法通常会返回由 repository 管理的聚合根(aggregate root)的一个或多个实例。 然而,有时可能需要根据这些类型的某些属性来创建投影。 Spring Data允许建模专用的返回类型,以更有选择地检索管理的聚合体(aggregate)的部分视图。
想象一下,一个repository和聚合根类型,如下面的例子。
class Person {
@Id UUID id;
String firstname, lastname;
Address address;
static class Address {
String zipCode, city, street;
}
}
interface PersonRepository extends Repository<Person, UUID> {
Collection<Person> findByLastname(String lastname);
}
现在想象一下,我们只想检索这个人的 name 属性。Spring Data提供了什么手段来实现这一点?本章的其余部分将回答这个问题。
5.1. 基于接口的投影
将查询结果限制在只有名称属性的最简单的方法是声明一个接口,为要读取的属性公开 accessor 方法,如下面的例子中所示。
interface NamesOnly {
String getFirstname();
String getLastname();
}
这里重要的一点是,这里定义的属性与聚合根(aggregate root)中的属性完全匹配。这样做可以添加一个查询方法,如下所示。
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
查询执行引擎在运行时为每个返回的元素创建该接口的代理实例,并将对公开方法的调用转发给目标对象。
在你的 Repository 中声明一个复写base方法的方法(例如在 CrudRepository 中声明的,一个特定store的repository接口,或 Simple…Repository),会导致对base方法的调用,不管声明的返回类型如何。请确保使用一个兼容的返回类型,因为base方法不能用于投影。一些store模块支持 @Query 注解,将重载的base方法变成查询方法,然后可以用来返回投影。
|
投影可以被递归使用。如果你想同时包括一些 Address 信息,为其创建一个投影接口,并从 getAddress() 的声明中返回该接口,如以下例子所示。
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
在方法调用时,目标实例的 address 属性被获取,并依次封装成一个投影代理。
5.1.1. 封闭的投影
一个投影接口,其访问器(accessor)方法都与目标集合的属性相匹配,被认为是一个封闭的投影。下面的例子(我们在本章前面也用过)就是一个封闭投影。
interface NamesOnly {
String getFirstname();
String getLastname();
}
如果你使用一个封闭的投影,Spring Data可以优化查询的执行,因为我们知道所有需要支持投影代理的属性。关于这一点的更多细节,请参见参考文档中的特定模块部分。
5.1.2. 开放的投影
投影接口中的访问器方法也可以通过使用 @Value 注解来计算新的值,如下面的例子中所示。
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
支持投影的聚合根(aggregate root)在 target 变量中可用。使用 @Value 的投影接口是一个开放的投影。在这种情况下,Spring Data不能应用查询执行优化,因为SpEL表达式可以使用聚合根的任何属性。
在 @Value 中使用的表达式不应过于复杂—你要避免在 String 变量中编程。对于非常简单的表达式,一种选择可能是求助于默认方法(在Java 8中引入),如下面的例子所示。
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}
这种方法要求你能够纯粹地根据投影接口上暴露的其他访问器方法来实现逻辑。第二个更灵活的选择是在Spring Bean中实现自定义逻辑,然后从SpEL表达式中调用该逻辑,如下面的例子所示。
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
请注意SpEL表达式是如何引用 myBean 并调用 getFullName(…) 方法和转发投影目标作为方法参数的。由SpEL表达式评估支持的方法也可以使用方法参数,然后可以从表达式中引用这些参数。方法参数可以通过一个名为 args 的 Object 数组获得。下面的例子显示了如何从 args 数组中获取方法参数。
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
同样,对于更复杂的表达式,你应该使用Spring Bean并让表达式调用一个方法,如 前所述。
5.1.3. Nullable Wrapper
投影接口中的Getter可以使用nullable的wrapper,以提高null的安全性。目前支持的wrapper类型有。
-
java.util.Optional -
com.google.common.base.Optional -
scala.Option -
io.vavr.control.Option
interface NamesOnly {
Optional<String> getFirstname();
}
如果底层投影值不是 null 的,那么将使用 wrapper 类型的当前表示法返回值。如果持有的值是 null 的,那么 getter 方法会返回所使用的 wrapper 类型的 null 表示。
5.2. 基于类的投影(DTO)
另一种定义投影的方法是使用value类型的DTO(数据传输对象),它持有应该被检索的字段的属性。这些DTO类型的使用方式与投影接口的使用方式完全相同,只是没有代理发生,也不能应用嵌套投影。
如果store通过限制要加载的字段来优化查询的执行,要加载的字段是由暴露出来的构造函数的参数名决定的。
下面的例子显示了一个投影的DTO。
class NamesOnly {
private final String firstname, lastname;
NamesOnly(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
String getFirstname() {
return this.firstname;
}
String getLastname() {
return this.lastname;
}
// equals(…) and hashCode() implementations
}
|
避免为投影DTO编写模板代码
你可以通过使用 Project Lombok 大大简化DTO的代码,它提供了一个
默认情况下,字段是 |
5.3. 动态投影
到目前为止,我们已经使用投影类型作为集合的返回类型或元素类型。然而,你可能想在调用时选择要使用的类型(这使它成为动态的)。为了应用动态投影,请使用一个查询方法,如下面的例子中所示。
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
这样,该方法可用于获得原样或应用投影的聚合体(aggregate),如以下例子所示。
void someMethod(PersonRepository people) {
Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}
对 Class 类型的查询参数进行检查,看它们是否符合动态投影参数的条件。如果查询的实际返回类型与 Class 参数的通用参数类型相同,那么匹配的 Class 参数就不能在查询或SpEL表达式中使用。如果你想使用一个 Class 参数作为查询参数,那么请确保使用不同的通用参数,例如 Class<?>。
|
6. Example 查询
6.1. 介绍
本章介绍了 "Example 查询" 并解释了如何使用它。
Example 查询(QBE)是一种用户友好的查询技术,接口简单。它允许动态查询创建,不要求你写包含字段名的查询。事实上,"Example 查询" 根本不要求你通过使用store特定的查询语言来编写查询。
6.2. 使用方式
Example 查询API由四部分组成。
-
Probe: 带有填充字段的domain对象的实际例子。 -
ExampleMatcher承载了如何匹配特定字段的细节。它可以在多个实例中重复使用。 -
Example: 一个Example由 probe 和ExampleMatcher组成。它被用来创建查询。 -
FetchableFluentQuery:FetchableFluentQuery提供了一个 fluent API,它允许进一步定制从Example衍生的查询。使用fluent API,你可以为你的查询指定排序投影和结果处理。
Example 查询很适合几种使用情况。
-
用一组静态或动态约束来查询你的data store。
-
频繁地重构domain对象而不用担心破坏现有的查询。
-
独立于底层 data store API 工作。
Example 查询也有一些限制。
-
不支持嵌套或分组的属性约束,如
firstname = ?0 or (firstname = ?1 and lastname = ?2).。 -
对于字符串只支持 开始/包含/结束/regex 匹配,对于其他属性类型支持精确匹配。
在开始使用Example 查询之前,你需要有一个domain对象。为了开始,为你的 repository 创建一个接口,如下面的例子所示。
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}
前面的例子显示了一个简单的domain对象。你可以用它来创建一个 Example。默认情况下,具有 null 值的字段会被忽略,而字符串则通过使用store特定的默认值进行匹配。
将属性纳入 Example 查询的标准是基于非 null 的。使用原始类型(int, double, …)的属性总是被包括在内,除非 ExampleMatcher 忽略了属性路径。
|
例子可以通过使用工厂方法或通过使用 ExampleMatcher 来构建。Example 是不可改变的。下面的列表显示了一个简单的例子。
Person person = new Person(); (1)
person.setFirstname("Dave"); (2)
Example<Person> example = Example.of(person); (3)
| 1 | 创建一个新的 domain 对象的实例。 |
| 2 | 设置要查询的属性。 |
| 3 | 创建 Example。 |
你可以通过使用 repository 来运行示例查询。要做到这一点,让你的 repository 接口扩展 QueryByExampleExecutor<T>。下面的列表显示了 QueryByExampleExecutor 接口的一个节选。
QueryByExampleExecutorpublic interface QueryByExampleExecutor<T> {
<S extends T> S findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
// … more functionality omitted.
}
6.3. Example Matcher
示例不限于默认设置。你可以通过使用 ExampleMatcher 为字符串匹配、null处理和特定属性设置指定你自己的默认值,如下面的例子所示。
Person person = new Person(); (1)
person.setFirstname("Dave"); (2)
ExampleMatcher matcher = ExampleMatcher.matching() (3)
.withIgnorePaths("lastname") (4)
.withIncludeNullValues() (5)
.withStringMatcher(StringMatcher.ENDING); (6)
Example<Person> example = Example.of(person, matcher); (7)
| 1 | 创建一个新的domain 象的实例。 |
| 2 | 设置属性。 |
| 3 | 创建一个 ExampleMatcher,期望所有的值都能匹配。在这个阶段,即使没有进一步的配置,它也是可用的。 |
| 4 | 构建一个新的 ExampleMatcher 来忽略 lastname 属性路径。 |
| 5 | 构建一个新的 ExampleMatcher,忽略 lastname 属性路径,并包含 null 值。 |
| 6 | 构建一个新的 ExampleMatcher,忽略 lastname 属性路径,包含 null 值,并进行后缀字符串匹配。 |
| 7 | 基于domain对象和配置的 ExampleMatcher 创建一个新的 Example。 |
默认情况下,ExampleMatcher 希望 probe 上设置的所有值都能匹配。如果你想得到与任何隐式定义的谓词(predicate)相匹配的结果,请使用 ExampleMatcher.matchingAny()。
你可以为单个属性(如 "firstname" 和 "lastname",或者对于嵌套属性,"address.city")指定行为。你可以用匹配选项和大小写敏感性来调整它,如下面的例子所示。
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}
配置 matcher 选项的另一种方法是使用 lambda (在Java 8中引入)。这种方法创建一个回调,要求实现者修改matcher。你不需要返回matcher,因为配置选项被保存在matcher实例中。下面的例子显示了一个使用lambda的matcher。
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}
Example 创建的查询使用的是配置的合并视图。默认的匹配设置可以在 ExampleMatcher 层面上设置,而个别设置可以应用于特定的属性路径。在 ExampleMatcher 上设置的设置会被属性路径设置所继承,除非它们被明确定义。属性补丁(patch)上的设置比默认设置有更高的优先权。下表描述了各种 ExampleMatcher 设置的范围。
| Setting | Scope |
|---|---|
Null-handling |
|
String matching |
|
Ignoring properties |
Property path |
Case sensitivity |
|
Value transformation |
Property path |
6.4. Fluent API
QueryByExampleExecutor 还提供了一个方法,我们到目前为止还没有提到。<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) 。和其他方法一样,它执行一个从 Example 派生的查询。然而,通过第二个参数,你可以控制该执行的各个方面,否则你无法动态地控制。你可以通过调用第二个参数中的 FetchableFluentQuery 的各种方法来做到这一点。 sortBy 让你为你的结果指定一个排序。as 让你指定你希望结果被转换的类型。project 限制了被查询的属性。first, firstValue, one, oneValue, all, page, stream, count, 和 exists 定义了你得到什么样的结果以及当超过预期结果数量时查询的行为方式。
Optional<Person> match = repository.findBy(example,
q -> q
.sortBy(Sort.by("lastname").descending())
.first()
);
7. 审计
7.1. 基础
Spring Data提供了复杂的支持,可以透明地跟踪谁创建或更改了实体以及更改发生的时间。为了从该功能中获益,你必须为你的实体类配备审计元数据,这些元数据可以使用注解或实现接口来定义。此外,审计必须通过注解配置或XML配置来启用,以注册所需的基础设施组件。关于配置样本,请参考特定store部分。
|
只跟踪创建和修改日期的应用程序不需要使其实体实现 |
7.1.1. 基于注解的审计元数据
我们提供 @CreatedBy 和 @LastModifiedBy 来捕获创建或修改实体的用户,以及 @CreatedDate 和 @LastModifiedDate 来捕获变化发生的时间。
class Customer {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
// … further properties omitted
}
正如你所看到的,注解可以有选择地应用,这取决于你想捕获哪些信息。这些注解,表示捕捉变化的时间,可以用在JDK8 date和time类型、long、Long 以及传统的Java Date 和 Calendar 的属性上。
审计元数据不一定需要存在于根级实体中,但可以添加到一个嵌入式实体中(取决于实际使用的store),如下面的片段所示。
class Customer {
private AuditMetadata auditingMetadata;
// … further properties omitted
}
class AuditMetadata {
@CreatedBy
private User user;
@CreatedDate
private Instant createdDate;
}
7.1.3. AuditorAware
如果你使用 @CreatedBy 或 @LastModifiedBy,审计基础设施需要以某种方式知道当前的principal。为此,我们提供了一个 AuditorAware<T> SPI接口,你必须实现这个接口来告诉基础设施谁是与应用程序交互的当前用户或系统。泛型 T 定义了用 @CreatedBy 或 @LastModifiedBy 注解的属性必须是什么类型。
下面的例子显示了一个使用Spring Security的 Authentication 对象的接口实现。
AuditorAware 的实现class SpringSecurityAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
该实现访问由Spring Security提供的 Authentication 对象,并查找你在 UserDetailsService 实现中创建的自定义 UserDetails 实例。我们在这里假设你是通过 UserDetails 实现来暴露domain用户的,但根据找到的 Authentication,你也可以从任何地方查到它。
7.1.4. ReactiveAuditorAware
当使用响应式基础设施时,你可能想利用上下文(Context)信息来提供 @CreatedBy 或 @LastModifiedBy 信息。我们提供了一个 ReactiveAuditorAware<T> SPI接口,你必须实现这个接口来告诉基础设施谁是当前与应用程序交互的用户或系统。泛型 T 定义了用 @CreatedBy 或 @LastModifiedBy 注释的属性必须是什么类型。
下面的例子显示了一个接口的实现,它使用了Spring Security的 Authentication 对象。
ReactiveAuditorAware 的实现class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {
@Override
public Mono<User> getCurrentAuditor() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
该实现访问由Spring Security提供的 Authentication 对象,并查找你在 UserDetailsService 实现中创建的自定义 UserDetails 实例。我们在这里假设你是通过 UserDetails 实现来暴露domain用户的,但根据找到的 Authentication,你也可以从任何地方查到它。
附录
Appendix A: 命名空间参考
<repositories /> 元素
<repositories /> 元素触发了Spring Data资源库基础设施的设置。最重要的属性是 base-package,它定义了扫描 Spring Data Repository 接口的包。参见 “XML 配置”。下表描述了 <repositories /> 元素的属性。
| Name | Description |
|---|---|
|
定义在自动检测模式下扫描扩展 |
|
定义用于自动检测自定义 repository 实现的后缀。名称以配置的后缀结尾的类被认为是候选类。默认为 |
|
确定用于创建 finder 查询的策略。详见 “Query 的查询策略” 。默认为 |
|
定义了搜索包含外部定义的查询的属性(Properties)文件的位置。 |
|
是否应该考虑嵌套的 repository 接口定义。默认为 |
Appendix B: Populators 命名空间参考
<populator /> 元素
<populator /> 元素允许通过Spring Data Repository 基础设施填充data store。[1]
| Name | Description |
|---|---|
|
在哪里可以找到从 repository 中读取对象的文件,应将其填充到 repository 中。 |
Appendix C: Repository query 关键字
支持的 query method subject 关键词
下表列出了Spring Data Repository 查询推导机制通常支持的subject关键字,以表达谓词。有关支持的关键字的确切列表,请查阅特定 store 的文档,因为这里列出的一些关键字可能在特定 store 中不被支持。
| Keyword | Description |
|---|---|
|
一般查询方法,通常返回 repository 类型、 |
|
Exists 投影,通常返回一个 |
|
返回数字结果的count投影。 |
|
删除查询方法要么不返回结果(void),要么返回删除数量。 |
|
将查询结果限制在第一个 |
|
使用 distinct 查询,只返回唯一的结果。请咨询特定store的文档是否支持该功能。这个关键字可以出现在 |
支持的查询方法 predicate 关键字和修饰语
下表列出了Spring Data Repository 查询推导机制通常支持的谓词(predicate)。但是,请查阅特定store的文档以了解支持的关键字的确切列表,因为这里列出的一些关键字可能在特定的store中不被支持。
| 逻辑关键字 | 关键字表达式 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
除了过滤谓词(predicate)外,还支持以下修饰语列表。
| Keyword | Description |
|---|---|
|
与谓语关键字一起使用,用于不区分大小写的比较。 |
|
忽略所有合适的属性的情况。在查询方法谓语的某处使用。 |
|
指定一个静态的排序顺序,后面是属性路径和方向(如 |
Appendix D: Repository 查询返回类型
支持的查询返回类型
下表列出了Spring Data Repository 通常支持的返回类型。但是,请查阅store的具体文档以了解支持的返回类型的确切列表,因为这里列出的一些类型可能不被特定的store所支持。
地理空间类型(如 GeoResult、GeoResults 和 GeoPage)仅适用于支持地理空间查询的data store。一些store模块可以定义他们自己的结果包装类型。
|
| 返回类型 | 说明 |
|---|---|
|
表示没有返回值。 |
Primitives |
Java 基本类型。 |
Wrapper types |
Java包装类型。 |
|
一个唯一的实体。期望查询方法最多返回一个结果。如果没有找到结果,则返回 |
|
迭代器 |
|
集合 |
|
列表 |
|
一个Java 8或 Guava 的 |
|
Scala 或 Vavr 的 |
|
Java 8 的 |
|
|
实现 |
暴露了一个构造函数或 |
Vavr |
Vavr集合类型。详见 支持 Vavr 集合。 |
|
一个 |
|
一个Java 8 |
|
有大小的数据块,显示是否有更多的数据可用。需要一个 |
|
带有附加信息的 |
|
带有附加信息的结果条目,如与参考位置的距离。 |
|
带有附加信息的 |
|
带有 |
|
Project Reactor |
|
Project Reactor |
|
RxJava |
|
RxJava |
|
RxJava |