# 前言
Spring Data Commons 项目将核心 Spring 概念应用于使用许多关系和非关系数据存储的解决方案的开发。
## 1. 项目元数据
* 版本控制:[https://github.com/spring-projects/spring-data-commons](https://github.com/spring-projects/spring-data-commons)
* Bugtracker:[https://github.com/spring-projects/spring-data-commons/issues](https://github.com/spring-projects/spring-data-commons/issues)
* 发布存储库:[https://repo.spring.io/libs-release](https://repo.spring.io/libs-release)
* 里程碑存储库:[https://repo.spring.io/libs-milestone](https://repo.spring.io/libs-milestone)
* 快照存储库:[https://repo.spring.io/libs-snapshot](https://repo.spring.io/libs-snapshot)
## 2. 依赖关系
由于每个 Spring 数据模块的启动日期不同,它们中的大多数都带有不同的主要版本号和次要版本号。找到兼容版本的最简单的方法是依赖 Spring 数据发布列 BOM,我们提供的是定义的兼容版本。在 Maven 项目中,你将在 POM 的``部分中声明此依赖项,如下所示:
例 1。使用 Spring 数据发布列表 BOM
```
org.springframework.data
spring-data-bom
2021.1.2
import
pom
```
当前的发行版本为`2021.1.2`。列车版本使用[calver](https://calver.org/)的模式`YYYY.MINOR.MICRO`。对于 GA 版本和服务版本,版本名如下:`${calver}`,对于所有其他版本,版本名如下:`${calver}-${modifier}`,其中`modifier`可以是以下几种类型之一:
* `SNAPSHOT`:当前快照
* `M1`,`M2`,以此类推:里程碑
* `RC1`,`RC2`,以此类推:释放候选项
你可以在[Spring Data examples repository](https://github.com/spring-projects/spring-data-examples/tree/master/bom)中找到使用 BOMS 的工作示例。有了这一点,你就可以在``块中声明希望使用的 Spring 数据模块,而不使用版本,如下所示:
例 2。声明对 Spring 数据模块的依赖项
```
org.springframework.data
spring-data-jpa
```
### 2.1.具有 Spring 引导的依赖管理
Spring 引导为你选择 Spring 数据模块的最新版本。如果你仍然希望升级到较新的版本,请将`spring-data-releasetrain.version`属性设置为希望使用的[训练版本和迭代](#dependencies.train-version)。
### 2.2. Spring 框架
当前版本的 Spring 数据模块需要 Spring 框架 5.3.16 或更好。这些模块还可以与该小版本的旧 Bugfix 版本一起工作。但是,强烈建议你在这一代中使用最新的版本。
## 3. 对象映射基础
本节介绍了 Spring 数据对象映射、对象创建、字段和属性访问、可变性和不可变性的基本原理。注意,本节仅适用于不使用底层数据存储的对象映射的 Spring 数据模块(如 JPA)。还要确保查阅存储特定的部分以获得存储特定的对象映射,例如索引、自定义列或字段名称等。
Spring 数据对象映射的核心职责是创建域对象的实例,并将存储本机数据结构映射到这些实例上。这意味着我们需要两个基本步骤:
1. 通过使用公开的构造函数之一创建实例。
2. 实例填充以实体化所有公开的属性。
### 3.1.对象创建
Spring 数据自动地尝试检测用于实现该类型对象的持久性实体的构造函数。分辨率算法的工作原理如下:
1. 如果只有一个构造函数,就使用它。
2. 如果有多个构造函数,并且正好有一个用`@PersistenceConstructor`注释,则使用它。
3. 如果有一个无参数构造函数,就使用它。其他构造函数将被忽略。
值解析假定构造函数参数名称与实体的属性名称匹配,即解析将被执行,就像要填充属性一样,包括映射中的所有自定义(不同的数据存储栏或字段名称等)。这还需要类文件中可用的参数名称信息,或者构造函数中存在`@ConstructorProperties`注释。
值分辨率可以通过使用 Spring Framework 的`@Value`使用特定于存储的 SPEL 表达式的值注释来定制。请参阅有关商店特定映射的部分以获取更多详细信息。
对象创建内部
Spring 为了避免反射的开销,数据对象创建默认使用在运行时生成的工厂类,它将直接调用域类构造函数。例如,对于这个示例类型:
```
class Person {
Person(String firstname, String lastname) { … }
}
```
我们将在运行时创建一个在语义上与这个类等价的工厂类:
```
class PersonObjectInstantiator implements ObjectInstantiator {
Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}
```
这给了我们一个迂回的 10% 的性能提升超过反映。为了使域类有资格进行这种优化,它需要遵守一组约束:
* 它一定不是一个私人班级。
* 它不能是非静态的内部类
* 它一定不是一个 CGlib 代理类
* Spring 数据使用的构造函数不能是私有的
如果这些条件中的任何一个匹配, Spring 数据将通过反射返回到实体实例化。
### 3.2.财产人口
一旦创建了实体的实例, Spring 数据就会填充该类的所有剩余的持久性属性。除非已经由实体的构造函数填充(即通过其构造函数参数列表填充),否则将首先填充标识符属性,以允许解析循环对象引用。在此之后,构造函数尚未填充的所有非瞬态属性都将在实体实例上设置。为此,我们使用以下算法:
1. 如果属性是不可变的,但是公开了`with…`方法(见下文),那么我们使用`with…`方法使用新的属性值创建一个新的实体实例。
2. 如果定义了属性访问(即通过 getter 和 setter 进行访问),那么我们调用的是 setter 方法。
3. 如果属性是可变的,我们直接设置字段。
4. 如果该属性是不可变的,那么我们将使用持久化操作使用的构造函数(参见[对象创建](#mapping.object-creation))来创建实例的副本。
5. 默认情况下,我们直接设置字段的值。
财产人口内部
与我们的[对象构造中的优化](#mapping.object-creation.details)类似,我们也使用 Spring 数据运行时生成的访问器类与实体实例交互。
```
class Person {
private final Long id;
private String firstname;
private @AccessType(Type.PROPERTY) String lastname;
Person() {
this.id = null;
}
Person(Long id, String firstname, String lastname) {
// Field assignments
}
Person withId(Long id) {
return new Person(id, this.firstname, this.lastame);
}
void setLastname(String lastname) {
this.lastname = lastname;
}
}
```
例 3。生成的属性访问器
```
class PersonPropertyAccessor implements PersistentPropertyAccessor {
private static final MethodHandle firstname; (2)
private Person person; (1)
public void setProperty(PersistentProperty property, Object value) {
String name = property.getName();
if ("firstname".equals(name)) {
firstname.invoke(person, (String) value); (2)
} else if ("id".equals(name)) {
this.person = person.withId((Long) value); (3)
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value); (4)
}
}
}
```
|**1**|PropertyAccessor 持有底层对象的可变实例。这是为了使本来不可变的属性发生突变。|
|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|默认情况下, Spring Data 使用字段访问来读写属性值。根据`private`字段的可见性规则,`MethodHandles`用于与字段交互。|
|**3**|该类公开了一个`withId(…)`方法,该方法用于设置标识符,例如,当一个实例被插入到数据存储中并生成了一个标识符时。调用`withId(…)`将创建一个新的`Person`对象。所有后续的突变都将发生在新实例中,而前一个实例将保持不变。|
|**4**|使用属性访问允许直接调用方法,而不使用`MethodHandles`。|
这给了我们一个迂回 25% 的性能提升超过反映。为了使域类有资格进行这种优化,它需要遵守一组约束:
* 类型不能驻留在默认值中或`java`包下。
* 类型及其构造函数必须`public`
* 内部类的类型必须是`static`。
* 使用的 Java 运行时必须允许在初始化`ClassLoader`中声明类。Java9 和更新版本施加了一定的限制。
默认情况下, Spring 数据尝试使用生成的属性访问器,如果检测到限制,则返回到基于反射的属性访问器。
让我们来看看下面这个实体:
例 4。一个样本实体
```
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**|标识符属性是最终的,但在构造函数中设置为`null`,
该类公开了一个`withId(…)`方法,该方法用于设置标识符,例如,当一个实例被插入到数据存储中并生成了一个标识符时。
当创建一个新的实例时,原始的`Person`实例保持不变。
相同的模式通常应用于其他属性 wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,并且设置该属性将被转换为创建一个新的实例,并应用新的标识符。|
|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|`firstname`和`lastname`属性是普通的不可变属性,可能通过 getter 公开。|
|**3**|`age`属性是一个不可变的属性,但它是从`birthday`属性派生出来的,
根据所示的设计,数据库值将超过默认值,因为 Spring 数据使用的是唯一声明的构造函数,
即使这样做的目的是为了更好地进行计算,重要的是,这个构造函数还将`age`作为参数(可能会忽略它),否则属性填充步骤将尝试设置 Age 字段并失败,因为它是不可变的,并且不存在`with…`方法。|
|**4**|`comment`属性 is mutable 是通过直接设置其字段来填充的。|
|**5**|`remarks`属性是可变的,可以通过直接设置`comment`字段或调用 setter 方法来填充|
|**6**|该类公开了用于创建对象的工厂方法和构造函数。
这里的核心思想是使用工厂方法而不是附加的构造函数,以避免通过`@PersistenceConstructor`消除构造函数歧义的需要。
相反,默认属性将在工厂方法中处理。|
### 3.3.一般性建议
* *尽量坚持使用不变的对象*—不可变对象很容易创建,因为物化一个对象只需要调用它的构造函数。此外,这也避免了你的域对象中充斥着允许客户端代码操作对象状态的 setter 方法。如果你需要这些,可以选择对它们进行包保护,以便它们只能被有限数量的合用类型调用。只有建造者的物化比财产人口快多达 30%。
* *提供一个 All-Args 构造器*——即使你不能或不想将实体建模为不可变的值,提供一个构造函数仍然有价值,该构造函数将实体的所有属性作为参数,包括可变的属性,因为这允许对象映射跳过属性总体以获得最佳性能。
* * 使用工厂方法而不是重载的构造函数来避免`@PersistenceConstructor`*——使用最佳性能所需的全参数构造函数,我们通常希望公开更多的应用程序用例特定的构造函数,这些构造函数省略了自动生成的标识符等内容。使用静态工厂方法来公开 All-Args 构造函数的这些变体是一种已有的模式。
* *确保遵守允许使用生成的实例器和属性访问器类的约束*—
* * 对于要生成的标识符,仍然使用与全参数持久性构造函数(首选)或`with…`方法 * 相结合的最终字段—
* *使用 Lombok 避免样板代码*—因为持久性操作通常需要一个构造函数来接受所有参数,所以它们的声明变成了对字段分配的样板参数的繁琐重复,而使用 Lombok 的`@AllArgsConstructor`可以最好地避免这种重复。
#### 3.3.1.覆盖属性
Java 允许域类的灵活设计,其中一个子类可以定义一个属性,该属性已经在其超类中以相同的名称声明了。考虑以下示例:
```
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 如果类型是不可分配的,则数据跳过超类型属性。也就是说,重写的属性的类型必须可分配给它的超类型属性类型,以注册为重写,否则超类型属性被认为是瞬态的。我们通常建议使用不同的属性名称。
Spring 数据模块通常支持持有不同值的重写属性。从编程模型的角度来看,有几点需要考虑:
1. 应该持久化哪个属性(默认为所有声明的属性)?你可以通过使用`@Transient`注释这些属性来排除这些属性。
2. 如何表示你的数据存储中的属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此你应该使用显式的字段/列名称对至少一个属性进行注释。
3. 使用`@AccessType(PROPERTY)`不能作为超级属性,如果不对 setter 实现做任何进一步的假设,通常不能进行设置。
### 3.4. Kotlin 支持
Spring 数据适应 Kotlin 的细节以允许对象创建和突变。
#### 3.4.1. Kotlin 对象创建 ###
Kotlin 支持实例化类,所有类在默认情况下都是不可变的,并且需要显式的属性声明来定义可变属性。考虑以下`data`类`Person`:
```
data class Person(val id: String, val name: String)
```
上面的类使用显式构造函数编译为一个典型的类。我们可以通过添加另一个构造函数来自定义这个类,并用`@PersistenceConstructor`对它进行注释,以表示构造函数的首选项:
```
data class Person(var id: String, val name: String) {
@PersistenceConstructor
constructor(id: String) : this(id, "unknown")
}
```
Kotlin 如果没有提供参数,则允许使用默认值,从而支持参数的可选性。当 Spring 数据检测到具有参数 default 的构造函数时,如果数据存储区不提供值(或简单地返回`null`),则不存在这些参数,因此 Kotlin 可以应用参数 default。考虑为`name`应用参数 default 的以下类
```
data class Person(var id: String, val name: String = "unknown")
```
每当`name`参数不是结果的一部分或其值`null`时,则`name`默认为`unknown`。
#### 3.4.2. Kotlin 数据类的属性总体 ####
在 Kotlin 中,所有类在默认情况下都是不可变的,并且需要显式的属性声明来定义可变属性。考虑以下`data`类`Person`:
```
data class Person(val id: String, val name: String)
```
这个类实际上是不可变的。它允许创建新实例,因为 Kotlin 生成了一个`copy(…)`方法,该方法创建新的对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用到该方法。
#### 3.4.3. Kotlin 最重要的属性
Kotlin 允许声明[属性重写](https://kotlinlang.org/docs/inheritance.html#overriding-properties)来改变子类中的属性。
```
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`上的 getters 和 setters 只设置`SubType.field`,而不是`SuperType.field`。在这种安排中,使用构造函数是设置`SuperType.field`的唯一缺省方法。将方法添加到`SubType`以通过`this.SuperType.field = …`设置`SuperType.field`是可能的,但不属于受支持的约定。属性重写在一定程度上造成了冲突,因为这些属性共享相同的名称,但可能表示两个不同的值。我们通常建议使用不同的属性名称。
Spring 数据模块通常支持持有不同值的重写属性。从编程模型的角度来看,有几点需要考虑:
1. 应该持久化哪个属性(默认为所有声明的属性)?你可以通过使用`@Transient`注释这些属性来排除这些属性。
2. 如何表示你的数据存储中的属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此你应该使用显式的字段/列名称对至少一个属性进行注释。
3. 不能使用`@AccessType(PROPERTY)`作为不能设置的超级属性。
## 4. 使用 Spring 数据存储库
Spring 数据存储库抽象的目标是显著减少为各种持久性存储实现数据访问层所需的样板代码的数量。
| |*Spring Data repository documentation and your module*
本章解释了 Spring 数据存储库的核心概念和接口。
本章中的信息是从 Spring 数据共享模块中提取的。
它使用了配置以及 Java 持久性 API( JPA)模块的代码示例。
你应该调整 XML 名称空间声明和要扩展的类型,使其与你所使用的特定模块的等同物。“[名称空间引用](#repositories.namespace-reference)”涵盖了 XML 配置,该配置在支持存储库 API 的所有 Spring 数据模块中都受到支持。“[存储库查询关键字](#repository-query-keywords)”一般涵盖了存储库抽象支持的查询方法关键字。
有关模块特定功能的详细信息,请参见本文档有关该模块的章节。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
### 4.1.核心概念
Spring 数据存储库抽象中的中心接口是`Repository`。它把要管理的域类以及域类的 ID 类型作为类型参数。这个接口主要充当一个标记接口,用于捕获要使用的类型,并帮助你发现扩展这个类型的接口。[`CrudRepository`](https://DOCS. Spring.io/ Spring-data/commons/DOCS/current/api/org/springframework/data/repository/crudrepository.html)接口为所管理的实体类提供了复杂的增删改查功能。
例 5。`CrudRepository`接口
```
public interface CrudRepository extends Repository {
S save(S entity); (1)
Optional findById(ID primaryKey); (2)
Iterable 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 的实体。|
| |我们还提供了特定于持久性技术的抽象,例如`JpaRepository`或`MongoRepository`。
这些接口扩展了`CrudRepository`,并且除了比较通用的持久性技术之外,还公开了底层持久性技术的功能,这些接口与持久性技术无关,例如`CrudRepository`。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
在`CrudRepository`之上,有一个[`PagingAndSortingRepository`](https://DOCS. Spring.io/ Spring-data/commons/DOCS/current/api/org/springframework/data/repository/pagingandsortingrepository.html)抽象,它添加了额外的方法,以简化对实体的分页访问:
例 6。`PagingAndSortingRepository`接口
```
public interface PagingAndSortingRepository extends CrudRepository {
Iterable findAll(Sort sort);
Page findAll(Pageable pageable);
}
```
要以 20 的页面大小访问`User`的第二页,可以执行以下操作:
```
PagingAndSortingRepository repository = // … get access to a bean
Page users = repository.findAll(PageRequest.of(1, 20));
```
除了查询方法之外,还可以对 Count 和 Delete 查询进行查询派生。下面的列表显示了派生的 Count 查询的接口定义:
例 7。派生计数查询
```
interface UserRepository extends CrudRepository {
long countByLastname(String lastname);
}
```
下面的清单显示了派生删除查询的接口定义:
例 8。派生删除查询
```
interface UserRepository extends CrudRepository {
long deleteByLastname(String lastname);
List removeByLastname(String lastname);
}
```
### 4.2.查询方法
标准增删改查功能存储库通常对底层数据存储进行查询。对于 Spring 数据,声明这些查询变成了一个四步过程:
1. 声明一个扩展存储库或其子接口之一的接口,并将其键入它应该处理的域类和 ID 类型,如以下示例所示:
```
interface PersonRepository extends Repository { … }
```
2. 在接口上声明查询方法。
```
interface PersonRepository extends Repository {
List findByLastname(String lastname);
}
```
3. 设置 Spring 来为这些接口创建代理实例,或者使用[JavaConfig](#repositories.create-instances.java-config),或者使用[XML 配置](#repositories.create-instances)。
1. 要使用 Java 配置,请创建一个类似于以下内容的类:
```
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
class Config { … }
```
2. 要使用 XML 配置,请定义类似于以下内容的 Bean:
```
```
JPA 名称空间在本例中使用。如果你对任何其他存储使用存储库抽象,则需要将其更改为存储模块的适当名称空间声明。换句话说,你应该使用`jpa`来交换,例如,`mongodb`。
另外,请注意,JavaConfig 变体不会显式地配置包,因为默认情况下使用的是带注释的类的包。要定制要扫描的包,请使用数据存储特定存储库的`basePackage…`-注释的`@Enable${store}Repositories`属性之一。
4. 注入存储库实例并使用它,如以下示例所示:
```
class SomeClient {
private final PersonRepository repository;
SomeClient(PersonRepository repository) {
this.repository = repository;
}
void doSomething() {
List persons = repository.findByLastname("Matthews");
}
}
```
下面的小节详细解释了每个步骤:
* [定义存储库接口](#repositories.definition)
* [定义查询方法](#repositories.query-methods.details)
* [创建存储库实例](#repositories.create-instances)
* [Custom Implementations for Spring Data Repositories](#repositories.custom-implementations)
### 4.3.定义存储库接口
要定义存储库接口,首先需要定义一个特定于域类的存储库接口。接口必须扩展`Repository`,并键入到域类和 ID 类型。如果希望公开该域类型的增删改查方法,请扩展`CrudRepository`,而不是`Repository`。
#### 4.3.1.微调存储库定义
通常,存储库接口扩展`Repository`、`CrudRepository`或`PagingAndSortingRepository`。或者,如果不想扩展 Spring 数据接口,也可以用`@RepositoryDefinition`注释存储库接口。扩展`CrudRepository`公开了一组完整的方法来操作你的实体。如果你希望对要公开的方法有所选择,那么可以将要公开的方法从`CrudRepository`复制到你的域存储库中。
| |这样做可以让你在所提供的 Spring 数据存储库功能之上定义自己的抽象。|
|---|-------------------------------------------------------------------------------------------------------------|
下面的示例显示了如何选择性地公开增删改查方法(在本例中,是`findById`和`save`):
例 9。有选择地暴露增删改查方法
```
@NoRepositoryBean
interface MyBaseRepository extends Repository {
Optional findById(ID id);
S save(S entity);
}
interface UserRepository extends MyBaseRepository {
User findByEmailAddress(EmailAddress emailAddress);
}
```
在前面的示例中,你为所有域存储库定义了一个公共的基本接口,并公开了`findById(…)`以及`save(…)`,这些方法被路由到你选择的存储库的基础存储库实现中,该存储库由 Spring 数据提供(例如,如果你使用 JPA,实现是`SimpleJpaRepository`),因为它们匹配`CrudRepository`中的方法签名。因此`UserRepository`现在可以保存用户,通过 ID 查找单个用户,并触发查询以通过电子邮件地址查找`Users`。
| |中间存储库接口用`@NoRepositoryBean`注释。
确保将该注释添加到所有存储库接口中,其中 Spring 数据不应在运行时为其创建实例。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 4.3.2.使用具有多个 Spring 数据模块的存储库
在应用程序中使用唯一的 Spring 数据模块使事情变得简单,因为定义的作用域中的所有存储库接口都绑定到 Spring 数据模块。有时,应用程序需要使用多个 Spring 数据模块。在这种情况下,存储库定义必须区分持久性技术。 Spring 当检测到类路径上的多个存储库工厂时,数据进入严格的存储库配置模式。严格配置使用存储库或域类的详细信息来决定 Spring 存储库定义的数据模块绑定:
1. 如果存储库定义[扩展特定于模块的存储库](#repositories.multiple-modules.types),则它是特定 Spring 数据模块的有效候选者。
2. 如果域类是[使用特定于模块的类型注释](#repositories.multiple-modules.annotations),则它是特定 Spring 数据模块的有效候选者。 Spring 数据模块要么接受第三方注释(例如 JPA 的`@Entity`),要么提供自己的注释(例如用于 Spring Data MongoDB 和 Spring Data ElasticSearch 的`@Document`)。
下面的示例展示了一个使用特定于模块的接口的存储库(本例中为 JPA):
例 10。使用特定于模块的接口的存储库定义
```
interface MyRepository extends JpaRepository { }
@NoRepositoryBean
interface MyBaseRepository extends JpaRepository { … }
interface UserRepository extends MyBaseRepository { … }
```
`MyRepository`和`UserRepository`在其类型层次结构中扩展`JpaRepository`。它们是 Spring 数据 JPA 模块的有效候选者。
下面的示例展示了一个使用通用接口的存储库:
例 11。使用通用接口的存储库定义
```
interface AmbiguousRepository extends Repository { … }
@NoRepositoryBean
interface MyBaseRepository extends CrudRepository { … }
interface AmbiguousUserRepository extends MyBaseRepository { … }
```
`AmbiguousRepository`和`AmbiguousUserRepository`在其类型层次结构中仅扩展`Repository`和`CrudRepository`。虽然在使用唯一的 Spring 数据模块时这是很好的,但多个模块无法区分这些存储库应该绑定到哪个特定的 Spring 数据。
下面的示例展示了一个使用带有注释的域类的存储库:
例 12。使用带有注释的域类的存储库定义
```
interface PersonRepository extends Repository { … }
@Entity
class Person { … }
interface UserRepository extends Repository { … }
@Document
class User { … }
```
`PersonRepository`引用`Person`,这是用 JPA `@Entity`注释的,所以这个存储库显然属于 Spring 数据 JPA。`UserRepository`引用了`User`,这是用 Spring Data MongoDB 的`@Document`注释的。
下面的糟糕示例展示了一个存储库,它使用带有混合注释的域类:
例 13。使用带有混合注释的域类的存储库定义
```
interface JpaPersonRepository extends Repository { … }
interface MongoDBPersonRepository extends Repository { … }
@Entity
@Document
class Person { … }
```
这个示例展示了一个同时使用 JPA 和 Spring 数据 MongoDB 注释的域类。它定义了两个存储库,`JpaPersonRepository`和`MongoDBPersonRepository`。一个用于 JPA,另一个用于 MongoDB 的使用。 Spring 数据不再能够区分存储库,这导致未定义的行为。
[存储库类型详细信息](#repositories.multiple-modules.types)和[区分域类注释](#repositories.multiple-modules.annotations)用于严格的存储库配置,以识别特定 Spring 数据模块的存储库候选。在同一域类型上使用多个特定于持久化技术的注释是可能的,并且允许跨多个持久化技术重用域类型。然而, Spring 这样的数据就不能再确定与存储库绑定的唯一模块。
区分存储库的最后一种方法是对存储库基包进行范围界定。基包定义了扫描存储库接口定义的起点,这意味着存储库定义位于适当的包中。默认情况下,注释驱动的配置使用配置类的包。[基于 XML 的配置中的基本包](#repositories.create-instances.spring)是强制性的。
下面的示例展示了基本包的注释驱动配置:
例 14。注解驱动的基包配置
```
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
```
### 4.4.定义查询方法
存储库代理有两种方式可以从方法名派生特定于存储的查询:
* 通过直接从方法名派生查询。
* 通过使用手动定义的查询。
可用的选项取决于实际的商店。但是,必须有一种策略来决定实际创建了什么查询。下一节描述了可用的选项。
#### 4.4.1.查询查找策略
以下策略可用于存储库基础结构来解析查询。使用 XML 配置,你可以通过`query-lookup-strategy`属性在名称空间配置策略。对于 Java 配置,可以使用`Enable${store}Repositories`注释的`queryLookupStrategy`属性。某些策略可能不支持特定的数据存储。
* `CREATE`尝试从查询方法名构造特定于存储的查询。一般的方法是从方法名称中删除一组已知的前缀,并解析方法的其余部分。你可以在“[查询创建](#repositories.query-methods.query-creation)”中阅读有关查询构造的更多信息。
* `USE_DECLARED_QUERY`尝试查找已声明的查询,如果找不到异常,则抛出异常。查询可以通过某个地方的注释来定义,也可以通过其他方式进行声明。请参阅特定存储的文档,以查找该存储的可用选项。如果存储库基础结构在引导阶段没有找到该方法的已声明查询,那么它将失败。
* `CREATE_IF_NOT_FOUND`(默认值)结合了`CREATE`和`USE_DECLARED_QUERY`。它首先查找已声明的查询,如果没有找到已声明的查询,则创建一个基于名称的自定义方法查询。这是默认的查找策略,因此,如果你没有显式地配置任何内容,就会使用该策略。它允许通过方法名快速定义查询,也可以根据需要引入声明的查询,从而对这些查询进行自定义优化。
#### 4.4.2.查询创建
Spring 数据存储库基础设施中内置的查询生成器机制对于在存储库的实体上构建约束查询非常有用。
下面的示例展示了如何创建许多查询:
例 15。从方法名创建查询
```
interface PersonRepository extends Repository {
List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List findByLastnameOrderByFirstnameAsc(String lastname);
List findByLastnameOrderByFirstnameDesc(String lastname);
}
```
解析查询方法名分为主语和谓语。第一部分(`find…By`,`exists…By`)定义了查询的主题,第二部分形成了谓词。引入子句(主语)可以包含更多的表达形式。在`find`(或其他引入关键字)和`By`之间的任何文本都被认为是描述性的,除非使用结果限制关键字之一,例如`Distinct`在要创建的查询上设置一个不同的标志或[`Top`/`First`限制查询结果]。
附录包含[查询方法主题关键字的完整列表](#appendix.query.method.subject)和[查询方法包括排序和大小写修饰符的谓词关键字](#appendix.query.method.predicate)。但是,第一个`By`充当分隔符,指示实际条件谓词的开始。在非常基本的级别上,你可以定义实体属性的条件,并将它们与`And`和`Or`连接起来。
解析该方法的实际结果取决于为其创建查询的持久性存储。然而,有一些一般性的事情需要注意:
* 表达式通常是属性遍历,与可以级联的运算符结合在一起。可以将属性表达式与`AND`和`OR`合并。对于属性表达式,还可以支持诸如`Between`、`LessThan`、`GreaterThan`和`Like`之类的运算符。受支持的操作符可以因数据存储而异,因此请参阅参考文档的相应部分。
* 方法解析器支持为单个属性(例如,`findByLastnameIgnoreCase(…)`)或支持忽略 case 的类型的所有属性(通常是`String`实例——例如,`findByLastnameAndFirstnameAllIgnoreCase(…)`)设置`IgnoreCase`标志。是否支持忽略情况可能会因存储而异,因此请参阅引用文档中的相关部分以获取特定于存储的查询方法。
* 你可以通过在引用属性的查询方法中附加`OrderBy`子句并提供排序方向(`Asc`或`Desc`)来应用静态排序。要创建支持动态排序的查询方法,请参见“[特殊参数处理](#repositories.special-parameters)”。
#### 4.4.3.属性表达式
属性表达式只能引用受管实体的直接属性,如前面的示例所示。在查询创建时,你已经确保解析的属性是托管域类的属性。但是,你也可以通过遍历嵌套属性来定义约束。考虑以下方法签名:
```
List findByAddressZipCode(ZipCode zipCode);
```
假设 a`Person`具有`Address`和`ZipCode`。在这种情况下,该方法创建`x.address.zipCode`属性遍历。解析算法首先将整个部分(`AddressZipCode`)解释为属性,然后检查域类中是否有该名称的属性(未大写)。如果算法成功,它将使用该属性。如果不是这样,则算法将右侧驼峰部分的源拆分为头部和尾部,并尝试找到相应的属性——在我们的示例中,`AddressZip`和`Code`。如果算法找到了一个带有头部的属性,它就会获取尾部,并继续从那里构建树,按照刚才描述的方式将尾部分割开来。如果第一次分割不匹配,则算法将分割点向左移动(`Address`,`ZipCode`)并继续。
尽管这在大多数情况下都适用,但算法可能会选择错误的属性。假设`Person`类也有一个`addressZip`属性。该算法将在第一轮分割中匹配,选择错误的属性,并失败(因为`addressZip`的类型可能没有`code`属性)。
要解决这种歧义,你可以在方法名中使用`_`来手动定义遍历点。因此,我们的方法名称如下:
```
List findByAddress_ZipCode(ZipCode zipCode);
```
因为我们将下划线字符视为保留字符,所以我们强烈建议遵循标准的 Java 命名约定(即不在属性名称中使用下划线,而是使用驼峰大小写)。
#### 4.4.4.特殊参数处理
要处理查询中的参数,请定义方法参数,如前面的示例中所示。除此之外,基础结构还可以识别某些特定类型,如`Pageable`和`Sort`,以便动态地对查询应用分页和排序。下面的示例演示了这些特性:
例 16。在查询方法中使用`Pageable`、`Slice`和`Sort`
```
Page findByLastname(String lastname, Pageable pageable);
Slice findByLastname(String lastname, Pageable pageable);
List findByLastname(String lastname, Sort sort);
List findByLastname(String lastname, Pageable pageable);
```
| |获取`Sort`和`Pageable`的 API 期望将非`null`值传递到方法中。
如果不想应用任何排序或分页,请使用`Sort.unsorted()`和`Pageable.unpaged()`。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
第一个方法允许你将`org.springframework.data.domain.Pageable`实例传递给查询方法,以动态地将分页添加到静态定义的查询中。a`Page`知道可用的元素和页面的总数。它通过触发 Count 查询来计算总数量的基础设施来实现这一点。因为这可能很昂贵(取决于使用的存储空间),所以你可以返回`Slice`。a`Slice`只知道 next`Slice`是否可用,当遍历更大的结果集时,这可能就足够了。
排序选项也通过`Pageable`实例处理。如果只需要排序,请向方法中添加`org.springframework.data.domain.Sort`参数。正如你所看到的,返回`List`也是可能的。在这种情况下,不会创建构建实际`Page`实例所需的附加元数据(这反过来意味着不会发出本来需要的附加计数查询)。相反,它将查询限制为仅查找给定的实体范围。
| |要找出整个查询有多少页,你必须触发一个额外的计数查询。
默认情况下,该查询是从你实际触发的查询派生出来的。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
##### 分页和排序
你可以使用属性名称来定义简单的排序表达式。你可以将表达式串联起来,以便将多个条件收集到一个表达式中。
例 17。定义排序表达式
```
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
```
要获得一种更安全的类型定义排序表达式的方法,请从要为其定义排序表达式的类型开始,并使用方法引用来定义要对其进行排序的属性。
例 18。使用类型安全的 API 定义排序表达式
```
TypedSort person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
```
| |`TypedSort.by(…)`通过(通常)使用 CGlib 来使用运行时代理,当使用诸如 Graal VM Native 之类的工具时,这可能会干扰本机图像编译。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
如果你的存储实现支持 QueryDSL,那么你也可以使用生成的元模型类型来定义排序表达式:
例 19。使用 QueryDSL API 定义排序表达式
```
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
```
#### 4.4.5.限制查询结果
你可以使用`first`或`top`关键字来限制查询方法的结果,这些关键字可以互换使用。可以在`top`或`first`中添加一个可选的数值,以指定要返回的最大结果大小。如果省略了这个数字,则假定结果大小为 1。下面的示例展示了如何限制查询大小:
例 20。用`Top`和`First`限制查询的结果大小
```
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page queryFirst10ByLastname(String lastname, Pageable pageable);
Slice findTop3ByLastname(String lastname, Pageable pageable);
List findFirst10ByLastname(String lastname, Sort sort);
List findTop10ByLastname(String lastname, Pageable pageable);
```
对于支持不同查询的数据存储,Limiting 表达式还支持`Distinct`关键字。此外,对于将结果集限制为一个实例的查询,支持用`Optional`关键字包装结果。
如果将分页或切片应用于限制性查询分页(以及可用页数的计算),则将在有限的结果中应用该分页。
| |通过使用`Sort`参数将结果与动态排序结合起来进行限制,这样就可以表示最小的“k”元素和最大的“k”元素的查询方法。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 4.4.6.返回集合或迭代的存储库方法
返回多个结果的查询方法可以使用标准的 Java`Iterable`、`List`和`Set`。除此之外,我们还支持返回 Spring 数据的`Streamable`、`Iterable`的自定义扩展以及[Vavr](https://www.vavr.io/)提供的集合类型。参考附录解释所有可能的[查询方法返回类型](#appendix.query.return.types)。
##### 使用 streamable 作为查询方法返回类型
你可以使用`Streamable`作为`Iterable`或任何集合类型的替代。它提供了方便的方法来访问非并行的`Stream`(从`Iterable`缺少)和直接`….filter(…)`和`….map(…)`上的元素并将`Streamable`连接到其他元素:
例 21。使用 streamable 组合查询方法的结果
```
interface PersonRepository extends Repository {
Streamable findByFirstnameContaining(String firstname);
Streamable findByLastnameContaining(String lastname);
}
Streamable result = repository.findByFirstnameContaining("av")
.and(repository.findByLastnameContaining("ea"));
```
##### 返回自定义的可刷新包装器类型
为集合提供专用的包装器类型是一种常用的模式,用于为返回多个元素的查询结果提供 API。通常,通过调用存储库方法返回类集合类型并手动创建包装器类型的实例来使用这些类型。你可以避免额外的步骤,因为 Spring Data 允许你使用这些包装器类型作为查询方法返回类型,如果它们满足以下条件的话:
1. 类型实现`Streamable`。
2. 该类型公开了一个构造函数或一个名为`of(…)`或`valueOf(…)`的静态工厂方法,该方法以`Streamable`为参数。
下面的清单展示了一个示例:
```
class Product { (1)
MonetaryAmount getPrice() { … }
}
@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable { (2)
private final Streamable streamable;
public MonetaryAmount getTotal() { (3)
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
@Override
public Iterator iterator() { (4)
return streamable.iterator();
}
}
interface ProductRepository implements Repository {
Products findAllByDescriptionContaining(String text); (5)
}
```
|**1**|允许 API 访问产品价格的`Product`实体。|
|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|一个`Streamable`的包装器类型,它可以通过使用`Products.of(…)`(使用 Lombok 注释创建的工厂方法)来构造。
一个使用`Streamable`的标准构造函数也可以这样做。|
|**3**|包装器类型公开了一个额外的 API,在`Streamable`上计算新值。|
|**4**|实现`Streamable`接口并将其委托给实际结果。|
|**5**|该包装器类型`Products`可以直接用作返回类型的查询方法。
你不需要返回`Streamable`并在存储库客户端中进行查询后手动包装它。|
##### 对 VAVR 收藏的支持
[Vavr](https://www.vavr.io/)是一个包含 Java 函数式编程概念的库。它附带了一组自定义的集合类型,你可以将其用作查询方法返回类型,如下表所示:
| Vavr collection type |使用的 VAVR 实现类型|Valid Java source types|
|------------------------|----------------------------------|-----------------------|
|`io.vavr.collection.Seq`|`io.vavr.collection.List`| `java.util.Iterable` |
|`io.vavr.collection.Set`|`io.vavr.collection.LinkedHashSet`| `java.util.Iterable` |
|`io.vavr.collection.Map`|`io.vavr.collection.LinkedHashMap`| `java.util.Map` |
你可以使用第一列中的类型(或其子类型)作为查询方法返回类型,并获取第二列中的类型作为实现类型,这取决于实际查询结果的 Java 类型(第三列)。或者,你可以声明`Traversable`(VAVR`Iterable`等价的),然后我们从实际的返回值派生实现类。即把 a`java.util.List`变成 vAVR`List`或`Seq`,a`java.util.Set`变成 vAVR`LinkedHashSet``Set`,以此类推。
#### 4.4.7.存储库方法的空处理
在 Spring Data2.0 中,返回单个聚合实例的存储库增删改查方法使用 Java8 的`Optional`来指示潜在的值缺失。此外, Spring Data 支持在查询方法上返回以下包装器类型:
* `com.google.common.base.Optional`
* `scala.Option`
* `io.vavr.control.Option`
或者,查询方法可以选择完全不使用包装器类型。然后通过返回`null`来表示没有查询结果。返回集合、集合替代方案、包装器和流的存储库方法保证永远不返回`null`,而是返回相应的空表示。详见“[存储库查询返回类型](#repository-query-return-types)”。
##### 可否定性注释
你可以通过使用[Spring Framework’s nullability annotations](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/core.html#null-safety)来表示存储库方法的可否定性约束。它们提供了一种工具友好的方法和 OPT-在运行时进行`null`检查,如下所示:
* [`@NonNullApi`](https://DOCS. Spring.io/ Spring/DOCS/5.3.16/javadoc-api/org/springframework/lang/nonnullapi.html):在包级别上用于声明参数和返回值的默认行为分别是既不接受也不产生`null`值。
* [`@NonNull`](https://DOCS. Spring.io/ Spring/DOCS/5.3.16/javadoc-api/org/springframework/lang/nonnull.html):用于参数或返回值,该参数或返回值必须不是`null`(对于`@NonNullApi`适用的参数和返回值不需要)。
* [`@Nullable`](https://DOCS. Spring.io/ Spring/DOCS/5.3.16/javadoc-api/org/springframework/lang/nullable.html):用于可以是`null`的参数或返回值。
Spring 注释是用[JSR 305](https://jcp.org/en/jsr/detail?id=305)注释(一种休眠但广泛使用的 JSR)进行元注释的。JSR305 元注释允许工具供应商(例如[IDEA](https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html)、[Eclipse](https://help.eclipse.org/oxygen/index.jsp?topic=/org.eclipse.jdt.doc.user/tasks/task-using_external_null_annotations.htm)和[Kotlin](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types))以通用方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。要为查询方法启用可否定性约束的运行时检查,你需要在包级别上通过在`package-info.java`中使用 Spring 的`@NonNullApi`来激活非可否定性,如以下示例所示:
例 22。在`package-info.java`中声明不可无效
```
@org.springframework.lang.NonNullApi
package com.acme;
```
一旦非空默认值到位,存储库查询方法调用将在运行时针对无效约束进行验证。如果查询结果违反了定义的约束,将引发异常。当方法返回`null`但被声明为 non-nullable 时,就会发生这种情况(缺省情况,在存储库所在的包上定义了注释)。如果你希望再次 OPT 到无效的结果,可以在单个方法上选择性地使用`@Nullable`。使用本节开头提到的结果包装器类型将继续按预期工作:将空结果转换为表示缺省的值。
下面的示例展示了刚才描述的一些技术:
例 23。使用不同的零度约束
```
package com.acme; (1)
import org.springframework.lang.Nullable;
interface UserRepository extends Repository {
User getByEmailAddress(EmailAddress emailAddress); (2)
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress); (3)
Optional findOptionalByEmailAddress(EmailAddress emailAddress); (4)
}
```
|**1**|存储库驻留在我们为其定义了非空行为的包(或子包)中。|
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|当查询不产生结果时抛出`EmptyResultDataAccessException`。当将`emailAddress`传递给方法的`null`时抛出`IllegalArgumentException`。|
|**3**|当查询不产生结果时,返回`null`。
还接受`null`作为`emailAddress`的值。|
|**4**|当查询不产生结果时返回`Optional.empty()`。当将`emailAddress`传递给方法的`null`时,抛出一个`IllegalArgumentException`。|
##### 基于 Kotlin 的存储库中的可否定性
Kotlin 已将[可否定性约束](https://kotlinlang.org/docs/reference/null-safety.html)的定义烘焙到语言中。 Kotlin 代码编译成字节码,其不通过方法签名而是通过编译元数据来表示可否定性约束。确保在你的项目中包含`kotlin-reflect` jar,以便能够对 Kotlin 的无效约束进行内省。 Spring 数据存储库使用语言机制来定义那些约束,以应用相同的运行时检查,如下所示:
例 24。在 Kotlin 存储库上使用可否定性约束
```
interface UserRepository : Repository {
fun findByUsername(username: String): User (1)
fun findByFirstname(firstname: String?): User? (2)
}
```
|**1**|该方法将参数和结果都定义为不可空( Kotlin 默认值)。
Kotlin 编译器拒绝将`null`传递给该方法的方法调用。
如果查询产生一个空结果,则抛出一个`EmptyResultDataAccessException`。|
|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|对于`firstname`参数,此方法接受`null`,如果查询不产生结果,则返回`null`。|
#### 4.4.8.流式查询结果
你可以通过使用 Java8`Stream`作为返回类型来增量地处理查询方法的结果。不是将查询结果包装在`Stream`中,而是使用特定于数据存储的方法来执行流,如以下示例所示:
例 25。用 java8`Stream`对查询结果进行流式处理
```
@Query("select u from User u")
Stream findAllByCustomQueryAndStream();
Stream readAllByFirstnameNotNull();
@Query("select u from User u")
Stream streamAllPaged(Pageable pageable);
```
| |`Stream`可能会包装基础数据存储特定的资源,因此在使用后必须关闭。
你可以通过使用`close()`方法或通过使用 Java7`try-with-resources`块手动关闭`Stream`,如以下示例所示:|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
例 26。与`Stream`一起工作会导致`try-with-resources`块
```
try (Stream stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
```
| |并非所有 Spring 数据模块当前都支持`Stream`作为返回类型。|
|---|---------------------------------------------------------------------------|
#### 4.4.9.异步查询结果
你可以使用[Spring’s asynchronous method running capability](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/integration.html#scheduling)异步运行存储库查询。这意味着当实际查询发生在已提交给 Spring `TaskExecutor`的任务中时,方法在调用后立即返回。异步查询与反应式查询不同,不应混用。有关反应性支持的更多详细信息,请参见特定于商店的文档。下面的示例显示了一些异步查询:
```
@Async
Future findByFirstname(String firstname); (1)
@Async
CompletableFuture findOneByFirstname(String firstname); (2)
@Async
ListenableFuture findOneByLastname(String lastname); (3)
```
|**1**|使用`java.util.concurrent.Future`作为返回类型。|
|-----|--------------------------------------------------------------------------------|
|**2**|使用 Java8`java.util.concurrent.CompletableFuture`作为返回类型。|
|**3**|使用`org.springframework.util.concurrent.ListenableFuture`作为返回类型。|
### 4.5.创建存储库实例
本节介绍如何为已定义的存储库接口创建实例和 Bean 定义。这样做的一种方法是使用每个支持存储库机制的 Spring 数据模块附带的 Spring 名称空间,尽管我们通常建议使用 Java 配置。
#### 4.5.1.XML 配置
Spring 每个数据模块包括一个`repositories`元素,该元素允许你定义一个基包,由 Spring 为你扫描,如以下示例所示:
例 27。通过 XML 启用 Spring 数据存储库
```
```
在前面的示例中, Spring 被指示扫描及其所有子包以用于扩展的接口或其一个子接口。对于找到的每个接口,基础结构注册了与持久性技术相关的`FactoryBean`,以创建处理查询方法调用的适当代理。每个 Bean 都注册在一个 Bean 名称下,该名称来自接口名称,因此`UserRepository`的接口将注册在`userRepository`下。 Bean 嵌套式存储库接口的名称以其封闭类型名称作为前缀。`base-package`属性允许通配符,这样你就可以定义扫描包的模式。
##### 使用过滤器
默认情况下,基础设施会获取扩展位于配置的基包下的特定于持久性技术的`Repository`子接口的每个接口,并为其创建一个 Bean 实例。然而,你可能希望对哪些接口为它们创建了 Bean 实例进行更细粒度的控制。为此,在``元素中使用``和``元素。语义与 Spring 上下文名称空间中的元素完全等价。有关这些元素的详细信息,请参见[Spring reference documentation](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/core.html#beans-scanning-filters)。
例如,为了将某些接口从作为存储库 bean 的实例化中排除,你可以使用以下配置:
例 28。使用排除过滤器元件
```
```
前面的示例排除了所有以`SomeRepository`结尾的接口的实例化。
#### 4.5.2.Java 配置
你还可以通过在 Java 配置类上使用特定于存储的`@Enable${store}Repositories`注释来触发存储库基础设施。有关 Spring 容器的基于 Java 的配置的介绍,请参见[JavaConfig in the Spring reference documentation](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/core.html#beans-java)。
启用 Spring 数据存储库的示例配置类似于以下内容:
例 29。基于注释的存储库配置示例
```
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
```
| |前面的示例使用特定于 JPA 的注释,你将根据实际使用的存储模块对其进行更改。这同样适用于`EntityManagerFactory` Bean 的定义。请参阅涵盖特定于商店的配置的部分。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 4.5.3.独立使用
你还可以在 Spring 容器之外使用存储库基础设施——例如,在 CDI 环境中。在你的 Classpath 中仍然需要一些 Spring 库,但是,通常情况下,你也可以通过编程的方式设置存储库。 Spring 提供存储库支持的数据模块附带你可以使用的持久性技术特有的`RepositoryFactory`,如下所示:
例 30。仓库工厂的独立使用
```
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
```
### 4.6. Spring 数据存储库的自定义实现
Spring 数据提供了各种选项,以用很少的编码来创建查询方法。但是当这些选项不适合你的需求时,你也可以为存储库方法提供自己的定制实现。这一节描述了如何做到这一点。
#### 4.6.1.自定义各个存储库
要用自定义功能丰富存储库,你必须首先为自定义功能定义一个片段接口和一个实现,如下所示:
例 31。自定义存储库功能的接口
```
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
```
例 32。自定义存储库功能的实现
```
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
```
| |与片段接口对应的类名中最重要的部分是`Impl`后缀。|
|---|-----------------------------------------------------------------------------------------------------------|
该实现本身不依赖于 Spring 数据并且可以是常规的 Spring Bean。因此,可以使用标准的依赖注入行为来注入对其他 bean 的引用(例如)、参与方面,等等。
然后,你可以让你的存储库接口扩展片段接口,如下所示:
例 33。对存储库接口的更改
```
interface UserRepository extends CrudRepository, CustomizedUserRepository {
// Declare query methods here
}
```
使用存储库接口扩展片段接口结合了增删改查和自定义功能,并使其对客户可用。
Spring 数据存储库是通过使用形成存储库组合的片段来实现的。片段是基本存储库、功能方面(如[QueryDsl](#core.extensions.querydsl))和自定义接口及其实现。每次向存储库接口添加一个接口时,都会通过添加一个片段来增强组合。基础存储库和存储库方面的实现是由每个 Spring 数据模块提供的。
下面的示例展示了自定义接口及其实现:
例 34。片段及其实现
```
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`的定制存储库的接口:
例 35。对存储库接口的更改
```
interface UserRepository extends CrudRepository, HumanRepository, ContactRepository {
// Declare query methods here
}
```
存储库可以由多个自定义实现组成,这些实现是按照其声明的顺序导入的。自定义实现比基本实现和存储库方面具有更高的优先级。这种排序使你可以重写基本存储库和方面方法,并且如果两个片段提供相同的方法签名,则可以解决歧义。存储库片段不限于在单个存储库接口中使用。多个存储库可能使用一个片段接口,允许你在不同的存储库之间重用定制。
下面的示例展示了一个存储库片段及其实现:
例 36。覆盖`save(…)`的片段
```
interface CustomizedSave {
S save(S entity);
}
class CustomizedSaveImpl implements CustomizedSave {
public S save(S entity) {
// Your custom implementation
}
}
```
下面的示例展示了一个使用前面的存储库片段的存储库:
例 37。定制的存储库接口
```
interface UserRepository extends CrudRepository, CustomizedSave {
}
interface PersonRepository extends CrudRepository, CustomizedSave {
}
```
##### 配置
如果你使用名称空间配置,存储库基础设施将通过扫描其发现存储库的包下面的类,尝试自动检测自定义实现片段。这些类需要遵循将命名空间元素的`repository-impl-postfix`属性追加到片段接口名称的命名惯例。此后缀缺省为`Impl`。下面的示例展示了一个使用默认后缀的存储库和一个为后缀设置自定义值的存储库:
例 38。配置示例
```
```
前面示例中的第一个配置试图查找一个名为`com.acme.repository.CustomizedUserRepositoryImpl`的类,以充当自定义存储库实现。第二个示例尝试查找`com.acme.repository.CustomizedUserRepositoryMyPostfix`。
###### 歧义的解决
如果在不同的包中发现了具有匹配的类名的多个实现,则 Spring 数据使用 Bean 名称来标识要使用哪个。
给出了下面两个用于`CustomizedUserRepository`的自定义实现,使用了第一个实现。 Bean 它的名称是`customizedUserRepositoryImpl`,它与片段接口的名称(`CustomizedUserRepository`)加上后缀`Impl`相匹配。
例 39。解决模棱两可的实现
```
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`中为存储库实现定义的名称匹配,并使用它来代替第一个。
###### 手动接线
Spring Bean Bean 如果你的自定义实现仅使用基于注释的配置和自动布线,则前面所示的方法工作得很好,因为它被视为任何其他方法。如果你的实现片段 Bean 需要特殊的接线,则可以声明 Bean 并根据[前一节](#repositories.single-repository-behaviour.ambiguity)中描述的约定对其进行命名。然后,基础设施通过名称引用手动定义的 Bean 定义,而不是创建本身。下面的示例展示了如何手动连接自定义实现:
例 40。自定义实现的手动接线
```
```
#### 4.6.2.自定义基本存储库
在[前一节](#repositories.manual-wiring)中描述的方法需要定制每个存储库接口,当你希望定制基本存储库行为时,所有存储库都会受到影响。为了改变所有存储库的行为,你可以创建一个实现来扩展特定于持久性技术的存储库基类。然后,这个类充当存储库代理的自定义基类,如下例所示:
例 41。自定义存储库基类
```
class MyRepositoryImpl
extends SimpleJpaRepository {
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 save(S entity) {
// implementation goes here
}
}
```
| |该类需要具有存储特定的存储库工厂实现所使用的超类的构造函数。
如果存储库基类具有多个构造函数,则重写一个`EntityInformation`加上一个存储特定的基础设施对象(例如`EntityManager`或模板类)。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
最后一步是使 Spring 数据基础设施了解定制的存储库基类。在 Java 配置中,你可以通过使用`@Enable${store}Repositories`注释的`repositoryBaseClass`属性来实现这一点,如下例所示:
例 42。使用 JavaConfig 配置自定义存储库基类
```
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
```
相应的属性在 XML 命名空间中可用,如以下示例所示:
例 43。使用 XML 配置自定义存储库基类
```
```
### 4.7.从聚合根发布事件
由存储库管理的实体是聚合根。在域驱动的设计应用程序中,这些聚合根通常发布域事件。 Spring 数据提供了一种名为`@DomainEvents`的注释,你可以在你的聚合根的方法上使用该注释,以使该发布尽可能简单,如以下示例所示:
例 44。从聚合根公开域事件
```
class AnAggregateRoot {
@DomainEvents (1)
Collection