Spring Data(数据) Couchbase

时间:2022-11-23 15:25:57

版本 5.0.0

Spring Data(数据) Couchbase

本参考文档描述了 Spring Data Couchbase 库的一般用法。

项目信息

  • 版本控制 -https://github.com/spring-projects/spring-data-couchbase
  • 错误跟踪器 -https://jira.springsource.org/browse/DATACOUCH
  • 发布存储库 -https://repo.spring.io/libs-release
  • 里程碑存储库 -https://repo.spring.io/libs-milestone
  • 快照存储库 -https://repo.spring.io/libs-snapshot

从 Spring Data Couchbase 3.x 迁移到 4.x

本章简要介绍了 4.x 中引入了哪些重大更改,并简要概述了迁移时要考虑的事项。

请注意,最低 Couchbase 服务器版本已隐含地提升到 5.5 及更高版本,我们建议至少运行 6.0.x。

配置

由于主要目标是从 Java SDK 2 迁移到 3,因此配置已更改以适应新的 SDK,并且从长远来看,还可以为范围和集合做好准备(但它仍然可以在没有集合支持的情况下使用)。

XML 配置支持已被删除,因此仅支持基于 java/注释的配置。

您的配置仍然需要扩展,但由于 RBAC(基于角色的访问控制)现在是必需的,因此需要覆盖不同的属性才能进行配置:,,和。如果要选择性地使用非默认作用域,可以重写该方法。请注意,如果要使用基于证书的身份验证或需要自定义密码身份验证,则可以重写该方法以执行此任务。​​AbstractCouchbaseConfiguration​​​​getConnectionString​​​​getUserName​​​​getPassword​​​​getBucketName​​​​getScopeName​​​​authenticator​

新 SDK 仍具有用于配置它的环境,因此您可以覆盖该方法并在需要时提供自定义配置。​​configureEnvironment​

有关更多信息,请参阅安装和配置。

Spring 引导版本兼容性

Spring Boot 2.3.x 或更高版本取决于 Spring Data Couchbase 4.x。早期版本的 Couchbase 不可用,因为 SDK 2 和 3 不能位于同一类路径上。

实体

如何处理实体没有改变,尽管由于SDK现在不再提供注释,因此仅支持与Spring-Data相关的注释。

具体说来:

  • ​com.couchbase.client.java.repository.annotation.Id​​成为import org.springframework.data.annotation.Id
  • ​com.couchbase.client.java.repository.annotation.Field​​成为import org.springframework.data.couchbase.core.mapping.Field

注释保持不变。​​org.springframework.data.couchbase.core.mapping.Document​

有关详细信息,请参阅实体建模。

自动索引管理

自动索引管理已经过重新设计,允许更灵活的索引编制。引入了新的注释,并删除了旧的注释。​​@ViewIndexed​​​​@N1qlSecondaryIndexed​​​​@N1qlPrimaryIndexed​

有关详细信息,请参阅自动索引管理。

模板和反应式模板

由于Couchbase SDK 3删除了对的支持,而是增加了对的支持,因此都可以直接从中访问。​​RxJava​​​​Reactor​​​​couchbaseTemplate​​​​reactiveCouchbaseTemplate​​​​AbstractCouchbaseConfiguration​

该模板已经过彻底修改,因此它现在使用流畅的 API 进行配置,而不是许多方法重载。这样做的好处是,将来我们能够扩展功能,而不必引入越来越多的重载,从而使导航变得复杂。

下表描述了 3.x 中的方法名称,并将它们与 4.x 等效项进行了比较:

表 1.模板方法比较

SDC 3.x

SDC 4.x

upsertById

插入

插入按ID

更新

replaceById

查找按ID

查找按ID

查找按视图

(已删除)

查找按空间视图

(已删除)

findByN1QL

查找按查询

findByN1QLProjection

查找按查询

查询N1QL

(直接调用SDK)

存在

existById

删除

删除按ID

执行

(直接调用SDK)

此外,还添加了以下在 3.x 中不可用的方法:

表 2.4.x 中的模板添加

名字

描述

removeByQuery

允许通过 N1QL 查询删除实体

查找比分析

通过分析服务执行查找

findFromReplicasById

像findById,但考虑了副本

我们尝试将 API 与底层 SDK 语义更紧密地统一和对齐,以便它们更易于关联和导航。

有关详细信息,请参阅模板和直接操作。

存储库和查询

  • ​org.springframework.data.couchbase.core.query.Query​​成为org.springframework.data.couchbase.repository.Query
  • ​org.springframework.data.couchbase.repository.ReactiveCouchbaseSortingRepository​​已被删除。考虑延长ReactiveSortingRepositoryReactiveCouchbaseRepository
  • ​org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository​​已被删除。考虑延长PagingAndSortingRepositoryCouchbaseRepository


删除了对视图的支持,N1QL 查询现在是所有自定义存储库方法以及默认情况下内置方法的一等公民。

与以前的版本相比,行为本身在查询派生应该如何工作方面没有变化。如果您遇到任何过去有效的查询,现在不再有效,请告诉我们。

可以通过 newannotation 覆盖 N1QL 查询的默认扫描一致性。​​ScanConsistency​

该方法也已删除。您仍然可以通过类器从本机 Java SDK 访问所有方法:​​getCouchbaseOperations()​​​​CouchbaseTemplate​​​​Cluster​

@Service
public class MyService {

@Autowired
private CouchbaseTemplate couchbaseTemplate;

@Autowired
private Cluster cluster;
}

有关更多信息,请参阅Couchbase 存储库。

全文搜索 (FTS)

FTS API 已简化,现在可以通过类访问:​​Cluster​

@Service
public class MyService {

@Autowired
private Cluster cluster;

public void myMethod() {
try {
final SearchResult result = cluster
.searchQuery("index", SearchQuery.queryString("query"));

for (SearchRow row : result.rows()) {
System.out.println("Found row: " + row);
}

System.out.println("Reported total rows: "
+ result.metaData().metrics().totalRows());
} catch (CouchbaseException ex) {
ex.printStackTrace();
}
}
}

有关详细信息,请参阅 FTS 文档。

1. 升级弹簧数据

有关如何从早期版本的 Spring 数据升级的说明在项目wiki 上提供。 按照发行说明部分中的链接查找要升级到的版本。

升级说明始终是发行说明中的第一项。如果您落后多个版本,请确保您还查看了您跳转的版本的发行说明。

Spring Data(数据) Couchbase

2. 安装和配置

本章介绍使用库时所需的常见安装和配置步骤。

2.1. 安装

所有用于生产使用的版本都分布在Maven Central和Spring版本存储库中。 因此,可以像任何其他 maven 依赖项一样包含该库:

例 1.包括通过 maven 的依赖关系

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-couchbase</artifactId>
<version>5.0.0</version>
</dependency>

这将引入几个依赖项,包括底层的Couchbase Java SDK,常见的Spring依赖项以及作为JSON映射基础结构的Jackson。

您还可以从 Spring 快照存储库( https://repo.spring.io/libs-snapshot ) 中获取快照,从Spring 里程碑存储库( https://repo.spring.io/libs-milestone ) 中获取里程碑版本。 下面是有关如何使用当前 SNAPSHOT 依赖项的示例:

例 2.使用快照版本

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-couchbase</artifactId>
<version>${version}-SNAPSHOT</version>
</dependency>

<repository>
<id>spring-libs-snapshot</id>
<name>Spring Snapshot Repository</name>
<url>https://repo.spring.io/libs-snapshot</url>
</repository>

一旦在类路径上有了所有需要的依赖关系,就可以开始配置它了。 仅支持 Java 配置(XML 配置已在 4.0 中删除)。

2.2. 基于注释的配置(“JavaConfig”)

首先,您需要做的就是子分支并实现抽象方法。​​AbstractCouchbaseConfiguration​

例 3.扩展​​AbstractCouchbaseConfiguration​

@Configuration
public class Config extends AbstractCouchbaseConfiguration {

@Override
public String getConnectionString() {
return "couchbase://127.0.0.1";
}

@Override
public String getUserName() {
return "Administrator";
}

@Override
public String getPassword() {
return "password";
}

@Override
public String getBucketName() {
return "travel-sample";
}
}

连接字符串由主机列表和可选方案 () 组成,如上面的代码所示。 您只需要提供一个要引导到的 Couchbase 节点列表(用 a 分隔)。请注意,虽然一个 主机在开发中就足够了,建议在这里添加 3 到 5 个引导节点。Couchbase 将拾取所有节点 自动从群集,但可能是您提供的唯一节点遇到问题 您正在启动应用程序。​​couchbase://​​​​,​

通过 RBAC(基于角色的访问控制)在 Couchbase Server 群集中配置的 Theandare。 反映要用于此配置的存储桶。​​userName​​​​password​​​​bucketName​

此外,可以通过重写 ato 返回已配置的方法来调整 SDK 环境。​​configureEnvironment​​​​ClusterEnvironment.Builder​​​​ClusterEnvironment​

从此配置中,可以自定义和覆盖更多内容作为自定义 Bean(例如存储库, 验证和自定义转换器)。

如果使用 and,则可能会遇到前缀为前缀的字段的问题。 由于 Spring Data Couchbase 默认将类型信息存储为 aattribute,这可能会有问题。 覆盖(例如返回)以更改 所述属性的名称。​​SyncGateway​​​​CouchbaseMobile​​​​_​​​​_class​​​​typeKey()​​​​MappingCouchbaseConverter.TYPEKEY_SYNCGATEWAY_COMPATIBLE​

如果启动应用程序,则应在日志中看到 Couchbase INFO 级别日志记录,指示底层 Couchbase Java SDK正在连接到数据库。如果报告了任何错误,请确保给定的凭据 并且主机信息正确。

3. 建模实体

本章介绍如何对实体进行建模,并解释它们在 Couchbase 服务器本身中的对应表示形式。

3.1. 对象映射基础

本节介绍了 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不变性的基础知识。 请注意,本节仅适用于不使用底层数据存储的对象映射(如 JPA)的 Spring 数据模块。 此外,请务必查阅特定于存储的部分,了解特定于存储的对象映射,例如索引、自定义列或字段名称等。

Spring 数据对象映射的核心职责是创建域对象的实例,并将存储本机数据结构映射到这些实例上。 这意味着我们需要两个基本步骤:

  1. 使用公开的构造函数之一创建实例。
  2. 实例填充以具体化所有公开的属性。

3.1.1. 对象创建

Spring Data 自动尝试检测持久实体的构造函数,以用于具体化该类型的对象。 解析算法的工作原理如下:

  1. 如果有一个静态工厂方法注释,则使用它。@PersistenceCreator
  2. 如果只有一个构造函数,则使用它。
  3. 如果有多个构造函数,并且只有一个被批注,则使用它。@PersistenceCreator
  4. 如果类型是 Java,则使用规范构造函数。Record
  5. 如果存在无参数构造函数,则使用它。 其他构造函数将被忽略。

值解析假定构造函数/工厂方法参数名称与实体的属性名称匹配,即解析将像要填充属性一样执行,包括映射中的所有自定义(不同的数据存储列或字段名称等)。 这还需要类文件中可用的参数名称信息或构造函数上存在的枚举注释。​​@ConstructorProperties​

值解析可以通过使用特定于商店的SpEL表达式使用Spring Framework的值注释来自定义。 有关更多详细信息,请参阅商店特定映射部分。​​@Value​

对象创建内部

为了避免反射的开销,Spring Data 对象创建默认使用运行时生成的工厂类,该工厂类将直接调用域类构造函数。 即对于此示例类型:

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 Data 要使用的构造函数不得是私有的

如果这些条件中的任何一个匹配,Spring 数据将通过反射回退到实体实例化。

3.1.2. 财产人口

创建实体的实例后,Spring Data 将填充该类的所有剩余持久属性。 除非已由实体的构造函数填充(即通过其构造函数参数列表使用),否则将首先填充标识符属性以允许解析循环对象引用。 之后,将在实体实例上设置构造函数尚未填充的所有非瞬态属性。 为此,我们使用以下算法:

  1. 如果属性是不可变的,但公开了 amethod(见下文),我们使用该方法创建一个具有新属性值的新实体实例。with…with…
  2. 如果定义了属性访问(即通过 getter 和 setter 的访问),我们将调用 setter 方法。
  3. 如果属性是可变的,我们直接设置字段。
  4. 如果属性是不可变的,我们将使用持久性操作要使用的构造函数(请参阅对象创建)来创建实例的副本。
  5. 默认情况下,我们直接设置字段值。

属性人口内部

与对象构造中的优化类似,我们还使用 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;
}
}

例 4.生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

private static final MethodHandle firstname;

private Person person;

public void setProperty(PersistentProperty property, Object value) {

String name = property.getName();

if ("firstname".equals(name)) {
firstname.invoke(person, (String) value);
} else if ("id".equals(name)) {
this.person = person.withId((Long) value);
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value);
}
}
}


PropertyAccessor 保存基础对象的可变实例。这是为了启用其他不可变属性的突变。


默认情况下,Spring 数据使用字段访问来读取和写入属性值。根据字段的可见性规则,用于与字段进行交互。​​private​​​​MethodHandles​


该类公开用于设置标识符的方法,例如,当实例插入数据存储并生成标识符时。调用创建一个新对象。所有后续突变都将发生在新实例中,而之前的突变保持不变。​​withId(…)​​​​withId(…)​​​​Person​


使用属性访问允许直接调用方法,而无需使用。​​MethodHandles​

这为我们提供了大约 25% 的性能提升。 要使域类符合此类优化的条件,它需要遵守一组约束:

  • 类型不得驻留在默认值或包下。java
  • 类型及其构造函数必须是public
  • 内部类的类型必须是。static
  • 使用的 Java 运行时必须允许在原始文件中声明类。Java 9 及更高版本施加了某些限制。ClassLoader

默认情况下,Spring Data 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的属性访问器。

让我们看一下以下实体:

例 5.示例实体

class Person {

private final @Id Long id;
private final String firstname, lastname;
private final LocalDate birthday;
private final int age;

private String comment;
private @AccessType(Type.PROPERTY) String remarks;

static Person of(String firstname, String lastname, LocalDate birthday) {

return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}

Person(Long id, String firstname, String lastname, LocalDate birthday, int age) {

this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}

Person withId(Long id) {
return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
}

void setRemarks(String remarks) {
this.remarks = remarks;
}
}

标识符属性是最终的,但在构造函数中设置为。 该类公开用于设置标识符的方法,例如,当实例插入数据存储并生成标识符时。 创建新实例时,原始实例保持不变。 相同的模式通常应用于存储管理的其他属性,但可能必须更改持久性操作。 wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,设置属性将转换为创建应用了新标识符值的新实例。​​null​​​​withId(…)​​​​Person​

和属性是普通的不可变属性,可能通过 getter 公开。​​firstname​​​​lastname​

属性是不可变的,但派生自属性。 按照所示的设计,数据库值将胜过默认值,因为 Spring Data 使用唯一声明的构造函数。 即使意图是首选计算,重要的是此构造函数也采用 as 参数(可能忽略它),否则属性填充步骤将尝试设置 age 字段并失败,因为它是不可变的并且不存在任何方法。​​age​​​​birthday​​​​age​​​​with…​

属性是可变的,通过直接设置其字段来填充。​​comment​

属性是可变的,并通过直接设置 thefield 或通过调用 setter 方法来填充​​remarks​​​​comment​

该类公开用于创建对象的工厂方法和构造函数。 这里的核心思想是使用工厂方法而不是其他构造函数,以避免通过构造函数消除歧义的需要。 相反,属性的默认值在工厂方法中处理。 如果您希望 Spring Data 使用工厂方法进行对象实例化,请使用 注释。​​@PersistenceCreator​​​​@PersistenceCreator​

3.1.3. 一般建议

  • 尝试坚持使用不可变对象 - 不可变对象很容易创建,因为具体化对象只需调用其构造函数即可。 此外,这可以避免域对象充斥着允许客户端代码操作对象状态的 setter 方法。 如果需要这些,最好使它们受到包保护,以便只能由有限数量的共存类型调用它们。 仅构造函数具体化比属性填充快 30%。
  • 提供全参数构造函数 — 即使不能或不想将实体建模为不可变值,提供将实体的所有属性(包括可变属性)作为参数的构造函数仍然有价值,因为这允许对象映射跳过属性填充以获得最佳性能。
  • 使用工厂方法而不是重载的构造函数来避免​​@PersistenceCreator​​ — 使用最佳性能所需的全参数构造函数,我们通常希望公开更多特定于应用程序用例的构造函数,这些构造函数省略了自动生成的标识符等内容。 这是一种既定模式,而是使用静态工厂方法来公开 all-args 构造函数的这些变体。
  • 确保遵守允许使用生成的实例化器和属性访问器类的约束
  • 对于要生成的标识符,仍将最终字段与全参数持久性构造函数(首选)或​​with​​...方法
  • 使用 Lombok 避免样板代码 — 由于持久性操作通常需要构造函数获取所有参数,因此它们的声明变成了对字段分配的繁琐重复,而使用 Lombok 可以最好地避免这些参数。@AllArgsConstructor
覆盖属性

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

这两个类都使用可赋值类型定义。 根据类设计,使用构造函数可能是唯一的默认设置方法。 或者,调用二传手可以设置。 所有这些机制在某种程度上都会产生冲突,因为属性共享相同的名称,但可能表示两个不同的值。 如果类型不可分配,则 Spring Data 将跳过超类型属性。 也就是说,重写属性的类型必须可分配给其超类型属性类型才能注册为重写,否则超类型属性被视为暂时性属性。 我们通常建议使用不同的属性名称。​​field​​​​SubType​​​​SuperType.field​​​​SuperType.field​​​​super.setField(…)​​​​field​​​​SuperType​

Spring 数据模块通常支持保存不同值的被覆盖属性。 从编程模型的角度来看,需要考虑以下几点:

  1. 应保留哪个属性(默认为所有声明的属性)? 您可以通过用这些属性批注来排除属性。@Transient
  2. 如何表示数据存储中的属性? 对不同的值使用相同的字段/列名称通常会导致数据损坏,因此应使用显式字段/列名称至少对其中一个属性进行批注。
  3. 不能使用,因为如果不对 setter 实现进行任何进一步的假设,通常无法设置超级属性。@AccessType(PROPERTY)

3.1.4. Kotlin 支持

Spring Data 调整了 Kotlin 的细节,以允许对象创建和更改。

Kotlin 对象创建

Kotlin 类支持实例化,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。

Spring Data 自动尝试检测持久实体的构造函数,以用于具体化该类型的对象。 解析算法的工作原理如下:

  1. 如果存在带有注释的构造函数,则使用它。@PersistenceCreator
  2. 如果类型是Kotlin 数据 cass,则使用主构造函数。
  3. 如果有一个静态工厂方法注释,则使用它。@PersistenceCreator
  4. 如果只有一个构造函数,则使用它。
  5. 如果有多个构造函数,并且只有一个被批注,则使用它。@PersistenceCreator
  6. 如果类型是 Java,则使用规范构造函数。Record
  7. 如果存在无参数构造函数,则使用它。 其他构造函数将被忽略。

请考虑以下类:​​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 检测到参数默认值的构造函数时,如果数据存储不提供值(或只是返回),则这些参数将保留为不存在,以便 Kotlin 可以应用参数默认值。请考虑以下应用参数默认值的类​​null​​​​name​

data class Person(var id: String, val name: String = "unknown")

每次参数不是结果的一部分或其值是时,则默认为。​​name​​​​null​​​​name​​​​unknown​

Kotlin 数据类的属性填充

在 Kotlin 中,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。 请考虑以下类:​​data​​​​Person​

data class Person(val id: String, val name: String)

此类实际上是不可变的。 它允许在 Kotlin 生成方法时创建新实例,该方法创建新的对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用于该方法。​​copy(…)​

Kotlin 覆盖属性

Kotlin 允许声明属性覆盖以更改子类中的属性。

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
SuperType(field) {
}

这种安排呈现两个具有名称的属性。 Kotlin 为每个类中的每个属性生成属性访问器(getter 和 setter)。 实际上,代码如下所示:​​field​

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;
}
}

吉特和二传手仅发病,不发病。 在这种安排中,使用构造函数是唯一要设置的默认方法。 可以添加方法toto setvia,但不属于支持的约定。 属性重写在某种程度上会产生冲突,因为属性共享相同的名称,但可能表示两个不同的值。 我们通常建议使用不同的属性名称。​​SubType​​​​SubType.field​​​​SuperType.field​​​​SuperType.field​​​​SubType​​​​SuperType.field​​​​this.SuperType.field = …​

Spring 数据模块通常支持保存不同值的被覆盖属性。 从编程模型的角度来看,需要考虑以下几点:

  1. 应保留哪个属性(默认为所有声明的属性)? 您可以通过用这些属性批注来排除属性。@Transient
  2. 如何表示数据存储中的属性? 对不同的值使用相同的字段/列名称通常会导致数据损坏,因此应使用显式字段/列名称至少对其中一个属性进行批注。
  3. 使用不能使用,因为无法设置超级属性。​​@AccessType(PROPERTY)​

3.2. 文档和字段

所有实体都应使用注释进行注释,但这不是必需的。​​@Document​

此外,实体中的每个字段都应使用注释进行注释。虽然这是 - 严格来说 - 可选,它有助于减少边缘情况,并清楚地显示实体的意图和设计。它也可以用来 将字段存储在其他名称下。​​@Field​

还有一个特殊的注释需要始终到位。最佳做法是同时命名属性。​​@Id​​​​id​

这是一个非常简单的实体:​​User​

例 6.带有字段的简单文档

@Document
public class User {

@Id
private String id;

@Field
private String firstname;

@Field
private String lastname;

public User(String id, String firstname, String lastname) {
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
}

public String getId() {
return id;
}

public String getFirstname() {
return firstname;
}

public String getLastname() {
return lastname;
}
}

Couchbase 服务器支持文档的自动过期。 库通过注释实现对它的支持。 您可以设置一个值,该值转换为自动删除文档之前的秒数。 如果要使其在突变后 10 秒后过期,请将其设置为类似。 或者,您可以使用 Spring 的属性支持和参数配置到期,以允许动态更改到期值。 例如:。 该属性必须可解析为 int 值,并且这两种方法不能混合使用。​​@Document​​​​expiry​​​​@Document(expiry = 10)​​​​expiryExpression​​​​@Document(expiryExpression = "${valid.document.expiry}")​

如果您希望文档中的字段名称与实体中使用的字段名称不同,则可以在注释上设置不同的名称。 例如,如果要保持文档较小,则可以将名字字段设置为。 在 JSON 文档中,您将看到代替。​​@Field​​​​@Field("fname")​​​​{"fname": ".."}​​​​{"firstname": ".."}​

注释需要存在,因为Couchbase中的每个文档都需要一个唯一的键。 此键必须是长度最多为 250 个字符的任何字符串。 随意使用适合您的用例的任何内容,无论是 UUID、电子邮件地址还是其他任何内容。​​@Id​

3.3. 数据类型和转换器

选择的存储格式是 JSON。它很棒,但像许多数据表示一样,它允许的数据类型比直接在 Java 中表达的数据类型少。 因此,对于所有非基元类型,需要进行某种形式的与受支持类型的转换。

对于以下实体字段类型,无需添加特殊处理:

表 3.基元类型

爪哇类型

JSON 表示形式

字符串

字符串

布尔

布尔

字节

国际

写入时忽略

由于JSON支持对象(“映射”)和列表,因此类型可以自然转换。 如果它们仅包含最后一段中的基元字段类型,则也无需添加特殊处理。 下面是一个示例:​​Map​​​​List​

例 7.包含地图和列表的文档

@Document
public class User {

@Id
private String id;

@Field
private List<String> firstnames;

@Field
private Map<String, Integer> childrenAges;

public User(String id, List<String> firstnames, Map<String, Integer> childrenAges) {
this.id = id;
this.firstnames = firstnames;
this.childrenAges = childrenAges;
}

}

存储包含一些示例数据的用户可能如下所示,如下所示:

例 8.包含映射和列表的文档 - JSON

{
"_class": "foo.User",
"childrenAges": {
"Alice": 10,
"Bob": 5
},
"firstnames": [
"Foo",
"Bar",
"Baz"
]
}

您不需要一直将所有内容分解为基元类型和列表/映射。 当然,您也可以从这些基元值中组合其他对象。 让我们修改最后一个示例,以便我们想要存储 aof:​​List​​​​Children​

例 9.包含组合对象的文档

@Document
public class User {

@Id
private String id;

@Field
private List<String> firstnames;

@Field
private List<Child> children;

public User(String id, List<String> firstnames, List<Child> children) {
this.id = id;
this.firstnames = firstnames;
this.children = children;
}

static class Child {
private String name;
private int age;

Child(String name, int age) {
this.name = name;
this.age = age;
}

}

}

填充的对象可能如下所示:

例 10.包含组合对象的文档 - JSON

{
"_class": "foo.User",
"children": [
{
"age": 4,
"name": "Alice"
},
{
"age": 3,
"name": "Bob"
}
],
"firstnames": [
"Foo",
"Bar",
"Baz"
]
}

大多数情况下,您还需要存储一个时态值,例如 a。 由于它不能直接存储在 JSON 中,因此需要进行转换。 该库实现了 和 JodaTime 类型的默认转换器(如果在类路径上)。 默认情况下,所有这些都在文档中表示为 unix 时间戳(数字)。 您始终可以使用自定义转换器覆盖默认行为,如下所示。 下面是一个示例:​​Date​​​​Date​​​​Calendar​

例 11.包含日期和日历的文档

@Document
public class BlogPost {

@Id
private String id;

@Field
private Date created;

@Field
private Calendar updated;

@Field
private String title;

public BlogPost(String id, Date created, Calendar updated, String title) {
this.id = id;
this.created = created;
this.updated = updated;
this.title = title;
}

}

填充的对象可能如下所示:

例 12.包含日期和日历的文档 - JSON

{
"title": "a blog post title",
"_class": "foo.BlogPost",
"updated": 1394610843,
"created": 1394610843897
}

(可选)通过将系统属性设置为 true,可以将日期与 ISO-8601 兼容字符串相互转换。 如果您想覆盖转换器或实现自己的转换器,这也是可能的。 该库实现了一般的弹簧转换器模式。 您可以在配置中创建 Bean 时插入自定义转换器。 以下是配置它的方法(在被覆盖中):​​org.springframework.data.couchbase.useISOStringConverterForDate​​​​AbstractCouchbaseConfiguration​

例 13.自定义转换器

@Override
public CustomConversions customConversions() {
return new CustomConversions(Arrays.asList(FooToBarConverter.INSTANCE, BarToFooConverter.INSTANCE));
}

@WritingConverter
public static enum FooToBarConverter implements Converter<Foo, Bar> {
INSTANCE;

@Override
public Bar convert(Foo source) {
return /* do your conversion here */;
}

}

@ReadingConverter
public static enum BarToFooConverter implements Converter<Bar, Foo> {
INSTANCE;

@Override
public Foo convert(Bar source) {
return /* do your conversion here */;
}

}

自定义转化需要注意以下几点:

  • 为了使其明确,请始终在转换器上使用注释。 特别是如果您正在处理基元类型转换,这将有助于减少可能的错误转换。@WritingConverter@ReadingConverter
  • 如果实现写入转换器,请确保仅解码为基元类型、映射和列表。 如果需要更复杂的对象类型,请使用底层翻译引擎也可以理解的 theandtypes。 最好的选择是坚持尽可能简单的转换。CouchbaseDocumentCouchbaseList
  • 始终在通用转换器之前放置更多特殊转换器,以避免执行错误转换器的情况。
  • 对于日期,读取转换器应该能够从任何(而不仅仅是)读取。 这是 N1QL 支持所必需的。NumberLong

3.4. 乐观锁定

在某些情况下,您可能希望确保在对文档执行更改操作时不会覆盖其他用户的更改。为此,您有三种选择:事务(自 Couchbase 6.5 以来)、悲观并发(锁定)或乐观并发。

乐观并发往往比悲观并发或事务提供更好的性能,因为不会对数据持有实际锁,也不会存储有关操作的额外信息(无事务日志)。

为了实现乐观锁定,Couchbase 使用 CAS(比较和交换)方法。当文档发生突变时,CAS 值也会更改。 CAS 对客户端是不透明的,您唯一需要知道的是,当内容或元信息也发生变化时,它会发生变化。

在其他数据存储中,可以通过具有递增计数器的任意版本字段来实现类似的行为。 由于Couchbase以更好的方式支持此功能,因此易于实现。 如果你想要自动乐观锁定支持,你需要做的就是在长字段上添加注释,如下所示:​​@Version​

例 14。具有乐观锁定的文档。

@Document
public class User {

@Version
private long version;

// constructor, getters, setters...
}

如果通过模板或存储库加载文档,版本字段将自动填充当前 CAS 值。 请务必注意,您不应访问该字段,甚至不应自行更改该字段。 保存回文档后,它要么成功,要么失败,并显示 a。 如果出现此类异常,则进一步的方法取决于要实现的应用程序。 您应该重试完整的加载-更新-写入周期,或者将错误传播到上层以进行正确处理。​​OptimisticLockingFailureException​

3.5. 验证

该库支持 JSR 303 验证,该验证直接基于实体中的注释。 当然,您可以在服务层中添加各种验证,但这样可以很好地耦合到您的实际实体。

To make it work, you need to include two additional dependencies. JSR 303 and a library that implements it, like the one supported by hibernate:

Example 15. Validation dependencies

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>

现在您需要在配置中添加两个 bean:

例 16。验证豆

@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}

@Bean
public ValidatingCouchbaseEventListener validationEventListener() {
return new ValidatingCouchbaseEventListener(validator());
}

现在,您可以使用 JSR303 注释来注释字段。 如果验证失败,则抛出 ais 。​​save()​​​​ConstraintViolationException​

例 17.示例验证注释

@Size(min = 10)
@Field
private String name;

3.6. 审计

可以通过 Spring 数据审计机制自动审核实体(跟踪哪个用户创建了对象、更新了对象以及在什么时间)。

首先,请注意,只有具有带注释字段的实体才能审核创建(否则框架会将创建解释为更新)。​​@Version​

审核的工作原理是用,,和的字段进行批注。 在保留实体时,框架将自动在这些字段上注入正确的值。 xxxDate 注释必须放在 afield(或兼容,例如 jodatime 类)上,而 xxxBy 注释可以放在任何类的字段上(尽管两个字段必须属于同一类型)。​​@CreatedBy​​​​@CreatedDate​​​​@LastModifiedBy​​​​@LastModifiedDate​​​​Date​​​​T​

要配置审计,首先需要在上下文中有一个审计员感知 Bean。 所述 bean 必须是类型(允许生成一个可以存储在我们之前看到的类型 xxxBy 字段中的值)。 其次,您必须使用注释在您的类中激活审计。​​AuditorAware<T>​​​​T​​​​@Configuration​​​​@EnableCouchbaseAuditing​

下面是一个示例:

例 18。审计实体示例

@Document
public class AuditedItem {

@Id
private final String id;

private String value;

@CreatedBy
private String creator;

@LastModifiedBy
private String lastModifiedBy;

@LastModifiedDate
private Date lastModification;

@CreatedDate
private Date creationDate;

@Version
private long version;

//..omitted constructor/getters/setters/...
}

请注意,两者都被放在远处,所以我们必须合作。​​@CreatedBy​​​​@LastModifiedBy​​​​String​​​​AuditorAware​​​​String​

例 19。审计员感知实施示例

public class NaiveAuditorAware implements AuditorAware<String> {

private String auditor = "auditor";

@Override
public String getCurrentAuditor() {
return auditor;
}

public void setAuditor(String auditor) {
this.auditor = auditor;
}
}

为了将所有这些联系在一起,我们使用 java 配置来声明 AuditorAware bean 并激活审计:

例 20。审计配置示例

@Configuration
@EnableCouchbaseAuditing //this activates auditing
public class AuditConfiguration extends AbstractCouchbaseConfiguration {

//... a few abstract methods omitted here

// this creates the auditor aware bean that will feed the annotations
@Bean
public NaiveAuditorAware testAuditorAware() {
return new NaiveAuditorAware();
}

4. 自动生成密钥

本章介绍如何使用内置机制自动生成 couchbase 文档键。 支持两种类型的自动生成策略。

  • 使用属性生成密钥
  • 使用 uuid 生成密钥

4.1. 配置

要自动生成的键应使用注释。 默认策略是。 键的前缀和后缀可以作为实体本身的一部分提供,这些值不会持久保存,它们仅用于密钥生成。 前缀和后缀使用值排序。 默认顺序是,没有顺序的多个前缀将覆盖前一个前缀。 如果 id 的值已可用,则将跳过自动生成。 可以使用提供用于串联的分隔符,默认分隔符是。​​@GeneratedValue​​​​USE_ATTRIBUTES​​​​order​​​​0​​​​delimiter​​​​.​

例 21。生成值的注释

@Document
public class User {
@Id @GeneratedValue(strategy = USE_ATTRIBUTES, delimiter = ".")
private String id;
@IdPrefix(order=0)
private String userPrefix;
@IdSuffix(order=0)
private String userSuffix;
...
}

4.2. 使用属性生成密钥

通常的做法是使用文档属性的组合生成密钥。 使用属性生成密钥会根据提供的类似于前缀和后缀的顺序连接所有带注释的属性值。​​IdAttribute​

例 22。IdAttribute 的注释

@Document
public class User {
@Id @GeneratedValue(strategy = USE_ATTRIBUTES)
private String id;
@IdAttribute
private String userid;
...
}

4.3. 使用 uuid 生成密钥

此自动生成使用 UUID 随机生成器生成占用 16 字节密钥空间的文档密钥。 此机制仅建议用于测试基架。

例 23。唯一键生成的注释

@Document
public class User {
@Id @GeneratedValue(strategy = UNIQUE)
private String id;
...
}

5. 使用 Spring 数据存储库

Spring 数据存储库抽象的目标是显著减少为各种持久性存储实现数据访问层所需的样板代码量。


Spring 数据存储库文档和您的模块



本章解释了 Spring 数据存储库的核心概念和接口。 本章中的信息来自 Spring 数据共享模块。 它使用 Jakarta 持久性 API (JPA) 模块的配置和代码示例。 如果要使用 XML 配置,则应调整 XML 命名空间声明和要扩展的类型,以使用的特定模块的等效项。“命名空间参考​”涵盖了XML配置,所有支持存储库API的Spring Data模块都支持XML配置。 “存储库查询关键字”涵盖了存储库抽象通常支持的查询方法关键字。 有关模块特定功能的详细信息,请参阅本文档有关该模块的章节。


5.1. 核心概念

Spring 数据存储库抽象中的中心接口是。 它采用要管理的域类以及域类的 ID 类型作为类型参数。 此接口主要充当标记接口,用于捕获要使用的类型,并帮助您发现扩展此接口的接口。 CrudRepository 和ListCrudRepository接口为正在管理的实体类提供了复杂的 CRUD 功能。​​Repository​

例 24.接口​​CrudRepository​

public interface CrudRepository<T, ID> extends Repository<T, ID> {

<S extends T> S save(S entity);

Optional<T> findById(ID primaryKey);

Iterable<T> findAll();

long count();

void delete(T entity);

boolean existsById(ID primaryKey);

// … more functionality omitted.
}

保存给定的实体。

返回由给定 ID 标识的实体。

返回所有实体。

返回实体数。

删除给定实体。

指示具有给定 ID 的实体是否存在。

​ListCrudRepository​​提供等效的方法,但它们返回方法返回 an。​​List​​​​CrudRepository​​​​Iterable​

我们还提供特定于持久性技术的抽象,例如 asor。 这些接口扩展并公开了底层持久性技术的功能,以及相当通用的持久性技术无关的接口,例如。​​JpaRepository​​​​MongoRepository​​​​CrudRepository​​​​CrudRepository​

除此之外,还有一个PagingAndSortingRepository抽象,它添加了其他方法来简化对实体的分页访问:​​CrudRepository​

示例 25.接口​​PagingAndSortingRepository​

public interface PagingAndSortingRepository<T, ID>  {

Iterable<T> findAll(Sort sort);

Page<T> findAll(Pageable pageable);
}

要访问页面大小为 20 的第二页,您可以执行以下操作:​​User​

PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));

除了查询方法之外,还可以对计数查询和删除查询进行查询派生。 以下列表显示了派生计数查询的接口定义:

例 26。派生计数查询

interface UserRepository extends CrudRepository<User, Long> {

long countByLastname(String lastname);
}

以下清单显示了派生删除查询的接口定义:

例 27。派生删除查询

interface UserRepository extends CrudRepository<User, Long> {

long deleteByLastname(String lastname);

List<User> removeByLastname(String lastname);
}

5.2. 查询方法

标准 CRUD 功能存储库通常对基础数据存储具有查询。 使用 Spring Data,声明这些查询变成了一个四步过程:

  1. 声明扩展存储库或其子接口之一的接口,并将其键入应处理的域类和 ID 类型,如以下示例所示:

interface PersonRepository extends Repository<Person, Long> { … }
  1. 在接口上声明查询方法。

interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLastname(String lastname);
}
  1. 设置 Spring 以使用JavaConfig或XML 配置为这些接口创建代理实例。

    爪哇岛
    .XML
@EnableJpaRepositories
class Config { … }

此示例中使用 JPA 命名空间。 如果将存储库抽象用于任何其他存储,则需要将其更改为存储模块的相应命名空间声明。 换句话说,您应该交换支持,例如,。jpamongodb

请注意,JavaConfig 变体不会显式配置包,因为缺省情况下使用带注释的类的包。 要自定义要扫描的包,请使用特定于数据存储的存储库注释的属性之一。basePackage…@EnableJpaRepositories

  1. 注入存储库实例并使用它,如以下示例所示:
class SomeClient {

private final PersonRepository repository;

SomeClient(PersonRepository repository) {
this.repository = repository;
}

void doSomething() {
List<Person> persons = repository.findByLastname("Matthews");
}
}

以下各节详细介绍了每个步骤:

  • 定义存储库接口
  • 定义查询方法
  • 创建存储库实例
  • Spring 数据存储库的自定义实现

5.3. 定义存储库接口

要定义存储库接口,首先需要定义特定于域类的存储库接口。 接口必须扩展并键入域类和 ID 类型。 如果要公开该域类型的 CRUD 方法,可以扩展或其变体之一,而不是。​​Repository​​​​CrudRepository​​​​Repository​

5.3.1. 微调存储库定义

您可以通过几种变体开始使用存储库界面。

典型的方法是扩展,这为您提供了 CRUD 功能的方法。 CRUD 代表 创建、读取、更新、删除。 在 3.0 版中,我们还引入了这与 但是对于那些返回多个实体的方法,它返回的是您可能发现更容易使用的方法。​​CrudRepository​​​​ListCrudRepository​​​​CrudRepository​​​​List​​​​Iterable​

如果您使用的是反应式存储,则可以选择,或者取决于您使用的反应式框架。​​ReactiveCrudRepository​​​​RxJava3CrudRepository​

如果你正在使用 Kotlin,你可以选择哪个利用 Kotlin 的协程。​​CoroutineCrudRepository​

此外,您还可以扩展,,,或者如果您需要允许指定抽象或在第一种情况下指定抽象的方法。 请注意,各种排序存储库不再像在 Spring 数据版本 3.0 之前那样扩展其各自的 CRUD 存储库。 因此,如果需要这两个接口的功能,则需要扩展这两个接口。​​PagingAndSortingRepository​​​​ReactiveSortingRepository​​​​RxJava3SortingRepository​​​​CoroutineSortingRepository​​​​Sort​​​​Pageable​

如果您不想扩展 Spring 数据接口,您还可以使用注释存储库接口。 扩展其中一个 CRUD 存储库接口会公开一组完整的方法来操作实体。 如果您希望对要公开的方法有选择性,请将要公开的方法从 CRUD 存储库复制到域存储库中。 执行此操作时,可以更改方法的返回类型。 如果可能,Spring 数据将遵循返回类型。 例如,对于返回多个实体的方法,您可以选择 VAVR 列表。​​@RepositoryDefinition​​​​Iterable<T>​​​​List<T>​​​​Collection<T>​

如果应用程序中的许多存储库应具有相同的方法集,则可以定义自己的基本接口进行继承。 必须对这样的接口进行注释。 这可以防止 Spring Data 尝试直接创建它的实例并失败,因为它无法确定该存储库的实体,因为它仍然包含一个通用类型变量。​​@NoRepositoryBean​

下面的示例演示如何有选择地公开 CRUD 方法(在本例中为):​​findById​​​​save​

例 28。有选择地公开 CRUD 方法

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

在前面的示例中,您为所有域存储库和公开以及定义了通用基本接口。这些方法被路由到 Spring Data 提供的您选择的存储的基本存储库实现中(例如,如果您使用 JPA,则实现是),因为它们与方法签名匹配。 因此,现在可以保存用户,按ID查找单个用户,并触发查询以按电子邮件地址查找。​​findById(…)​​​​save(…)​​​​SimpleJpaRepository​​​​CrudRepository​​​​UserRepository​​​​Users​

中间存储库接口带有注释。 确保将该注释添加到 Spring Data 在运行时不应为其创建实例的所有存储库接口。​​@NoRepositoryBean​

5.3.2. 使用具有多个 Spring 数据模块的存储库

在应用程序中使用唯一的 Spring 数据模块使事情变得简单,因为定义范围内的所有存储库接口都绑定到 Spring 数据模块。 有时,应用程序需要使用多个 Spring 数据模块。 在这种情况下,存储库定义必须区分持久性技术。 当它在类路径上检测到多个存储库工厂时,Spring Data 进入严格的存储库配置模式。 严格配置使用存储库或域类的详细信息来决定存储库定义的 Spring 数据模块绑定:

  1. 如果存储库定义扩展了特定于模块的存储库,则它是特定 Spring 数据模块的有效候选者。
  2. 如果域类使用特定于模块的类型注释进行注释,则它是特定 Spring 数据模块的有效候选者。 Spring Data 模块接受第三方注释(例如 JPA)或提供自己的注释(例如 Spring Data MongoDB 和 Spring Data Elasticsearch)。@Entity@Document

以下示例显示了使用特定于模块的接口(在本例中为 JPA)的存储库:

例 29。使用特定于模块的接口的存储库定义

interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }

interface UserRepository extends MyBaseRepository<User, Long> { … }

​MyRepository​​并扩展其类型层次结构。 它们是 Spring Data JPA 模块的有效候选者。​​UserRepository​​​​JpaRepository​

以下示例显示了使用通用接口的存储库:

例 30。使用通用接口的存储库定义

interface AmbiguousRepository extends Repository<User, Long> { … }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }

​AmbiguousRepository​​并仅在其类型层次结构中扩展。 虽然这在使用唯一的 Spring 数据模块时很好,但多个模块无法区分这些存储库应该绑定到哪个特定的 Spring 数据。​​AmbiguousUserRepository​​​​Repository​​​​CrudRepository​

以下示例显示了一个使用带有注释的域类的存储库:

例 31。使用带有注释的域类的存储库定义

interface PersonRepository extends Repository<Person, Long> { … }

@Entity
class Person { … }

interface UserRepository extends Repository<User, Long> { … }

@Document
class User { … }

​PersonRepository​​引用,它用JPAannotation注释,所以这个存储库显然属于Spring Data JPA.references,它用Spring Data MongoDB的sannotation注释。​​Person​​​​@Entity​​​​UserRepository​​​​User​​​​@Document​

以下错误示例显示了一个使用具有混合注释的域类的存储库:

例 32。使用具有混合注释的域类的存储库定义

interface JpaPersonRepository extends Repository<Person, Long> { … }

interface MongoDBPersonRepository extends Repository<Person, Long> { … }

@Entity
@Document
class Person { … }

这个例子展示了一个同时使用JPA和Spring Data MongoDB注释的域类。 它定义了两个存储库,并且。 一个用于JPA,另一个用于MongoDB使用。 Spring 数据不再能够区分存储库,这会导致未定义的行为。​​JpaPersonRepository​​​​MongoDBPersonRepository​

存储库类型详细信息和区分域类注释用于严格的存储库配置,以识别特定 Spring 数据模块的存储库候选者。 可以对同一域类型使用多个特定于持久性技术的注释,并允许跨多个持久性技术重用域类型。 但是,Spring Data 无法再确定绑定存储库的唯一模块。

区分存储库的最后一种方法是确定存储库基础包的范围。 基本包定义扫描存储库接口定义的起点,这意味着存储库定义位于相应的包中。 默认情况下,注释驱动的配置使用配置类的包。 基于 XML 的配置中的基本包是必需的。

以下示例显示了基本包的注释驱动配置:

例 33。注释驱动的基本包配置

@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }

5.4. 定义查询方法

存储库代理有两种方法可以从方法名称派生特定于存储的查询:

  • 通过直接从方法名称派生查询。
  • 通过使用手动定义的查询。

可用选项取决于实际商店。 但是,必须有一个策略来决定创建什么实际查询。 下一节介绍可用选项。

5.4.1. 查询查找策略

存储库基础结构可以使用以下策略来解析查询。 使用 XML 配置,您可以通过属性在命名空间中配置策略。 对于 Java 配置,您可以使用注释的属性。 特定数据存储可能不支持某些策略。​​query-lookup-strategy​​​​queryLookupStrategy​​​​EnableJpaRepositories​

  • ​CREATE​​尝试从查询方法名称构造特定于存储的查询。 一般方法是从方法名称中删除一组给定的已知前缀,并分析方法的其余部分。 您可以在“查询创建”中阅读有关查询构造的更多信息。
  • ​USE_DECLARED_QUERY​​尝试查找已声明的查询,如果找不到查询,则会引发异常。 查询可以通过某处的注释定义,也可以通过其他方式声明。 请参阅特定商店的文档以查找该商店的可用选项。 如果存储库基础结构在引导时找不到该方法的声明查询,则会失败。
  • ​CREATE_IF_NOT_FOUND​​(默认值)组合沙。 它首先查找已声明的查询,如果未找到已声明的查询,则会创建一个基于名称的自定义方法查询。 这是默认的查找策略,因此,如果未显式配置任何内容,则使用此方法。 它允许按方法名称快速定义查询,但也允许根据需要引入声明的查询来自定义调整这些查询。CREATEUSE_DECLARED_QUERY

5.4.2. 查询创建

Spring 数据存储库基础结构中内置的查询生成器机制对于构建对存储库实体的约束查询非常有用。

下面的示例演示如何创建多个查询:

例 34。从方法名称创建查询

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

分析查询方法名称分为主语和谓语。 第一部分 (,) 定义查询的主题,第二部分构成谓词。 引言子句(主语)可以包含进一步的表达式。 (或其他引入关键字)之间的任何文本都被视为描述性的,除非使用结果限制关键字之一,例如 ato 在要创建的查询上设置不同的标志或Top/First以限制查询结果。find…Byexists…ByfindByDistinct

附录包含查询方法主题关键字和查询方法谓词关键字的完整列表,包括排序和字母大小写修饰符。 但是,第一个充当分隔符来指示实际条件谓词的开始。 在非常基本的级别上,您可以定义实体属性的条件并将它们与 and 连接起来。​​By​​​​And​​​​Or​

分析方法的实际结果取决于为其创建查询的暂留存储。 但是,有一些一般事项需要注意:

  • 表达式通常是属性遍历与可以连接的运算符相结合。 可以将属性表达式与 and 组合在一起。 您还可以获得对运算符的支持,例如,,, 和属性表达式。 支持的运算符可能因数据存储而异,因此请参阅参考文档的相应部分。ANDORBetweenLessThanGreaterThanLike
  • 方法解析器支持为单个属性(例如)或支持忽略大小写的类型的所有属性(通常是实例 — 例如,)设置 anflag。 是否支持忽略案例可能因商店而异,因此请参阅特定于商店的查询方法的参考文档中的相关部分。IgnoreCasefindByLastnameIgnoreCase(…)StringfindByLastnameAndFirstnameAllIgnoreCase(…)
  • 可以通过将 anclause 追加到引用属性的查询方法并提供排序方向 (or) 来应用静态排序。 要创建支持动态排序的查询方法,请参阅 “特殊参数处理”。OrderByAscDesc

5.4.3. 属性表达式

属性表达式只能引用托管实体的直接属性,如前面的示例所示。 在创建查询时,已确保分析的属性是托管域类的属性。 但是,也可以通过遍历嵌套属性来定义约束。 请考虑以下方法签名:

List<Person> findByAddressZipCode(ZipCode zipCode);

假设 ahas anwith a。 在这种情况下,该方法将创建属性遍历。 解析算法首先将整个部件 () 解释为属性,并检查域类中是否存在具有该名称(未大写)的属性。 如果算法成功,它将使用该属性。 如果没有,该算法将右侧驼峰案例部分的源拆分为头部和尾部,并尝试找到相应的属性 — 在我们的示例中,and。 如果算法找到具有该头部的属性,它将获取尾部并继续从那里向下构建树,以刚才描述的方式将尾部拆分。 如果第一个拆分不匹配,算法会将拆分点向左移动 (,) 并继续。​​Person​​​​Address​​​​ZipCode​​​​x.address.zipCode​​​​AddressZipCode​​​​AddressZip​​​​Code​​​​Address​​​​ZipCode​

尽管这应该适用于大多数情况,但算法可能会选择错误的属性。 假设该类也有属性。 算法将在第一轮拆分中匹配,选择错误的属性,然后失败(因为类型可能没有属性)。​​Person​​​​addressZip​​​​addressZip​​​​code​

要解决这种歧义,您可以在方法名称中使用手动定义遍历点。 所以我们的方法名称如下:​​_​

List<Person> findByAddress_ZipCode(ZipCode zipCode);

由于我们将下划线字符视为保留字符,因此强烈建议遵循标准的 Java 命名约定(即,不要在属性名称中使用下划线,而是使用驼峰大小写)。

5.4.4. 特殊参数处理

若要处理查询中的参数,请定义方法参数,如前面的示例所示。 除此之外,基础架构还可以识别某些特定类型,例如and,以动态地将分页和排序应用于您的查询。 以下示例演示了这些功能:​​Pageable​​​​Sort​

例 35。使用 、 和 in 查询方法​​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()​

第一种方法允许您将实例传递给查询方法,以动态地将分页添加到静态定义的查询中。 了解可用元素和页面的总数。 它通过基础结构触发计数查询来计算总数。 由于这可能很昂贵(取决于所使用的商店),因此您可以改为返回 a。 仅知道 next是否可用,这在遍历较大的结果集时可能就足够了。​​org.springframework.data.domain.Pageable​​​​Page​​​​Slice​​​​Slice​​​​Slice​

排序选项也通过实例处理。 如果只需要排序,请向方法添加参数。 如您所见,返回 ais 也是可能的。 在这种情况下,不会创建构建实际实例所需的其他元数据(这反过来意味着不会发出所需的其他计数查询)。 相反,它将查询限制为仅查找给定范围的实体。​​Pageable​​​​org.springframework.data.domain.Sort​​​​List​​​​Page​

要了解整个查询获得的页面数,您必须触发额外的计数查询。 默认情况下,此查询派生自实际触发的查询。

分页和排序

可以使用属性名称定义简单的排序表达式。 您可以连接表达式以将多个条件收集到一个表达式中。

例 36。定义排序表达式

Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());

有关定义排序表达式的更类型安全的方法,请从定义排序表达式的类型开始,并使用方法引用定义要排序的属性。

例 37。使用类型安全的 API 定义排序表达式

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 本机等工具时可能会干扰本机映像编译。

如果您的商店实现支持 Querydsl,您还可以使用生成的元模型类型来定义排序表达式:

例 38。使用 Querydsl API 定义排序表达式

QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));

5.4.5. 限制查询结果

可以使用 theor关键字来限制查询方法的结果,这些关键字可以互换使用。 您可以附加一个可选的数值来指定要返回的最大结果大小。 如果省略该数字,则假定结果大小为 1。 以下示例演示如何限制查询大小:​​first​​​​top​​​​top​​​​first​

例 39。限制查询的结果大小​​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);

限制表达式还支持支持不同查询的数据存储的关键字。 此外,对于将结果集限制为一个实例的查询,支持使用 thekeyword 将结果包装到其中。​​Distinct​​​​Optional​

如果分页或切片应用于限制查询分页(以及可用页数的计算),则会在有限的结果中应用分页或切片。


通过使用参数限制结果和动态排序,可以表示“K”最小元素和“K”最大元素的查询方法。​​Sort​

5.4.6. 返回集合或可迭代对象的存储库方法

返回多个结果的查询方法可以使用标准 Java、and。 除此之外,我们还支持返回Spring Data,自定义扩展以及​​Vavr​​提供的集合类型。 请参阅解释所有可能的查询方法返回类型的附录。​​Iterable​​​​List​​​​Set​​​​Streamable​​​​Iterable​

使用可流式处理作为查询方法返回类型

您可以将其用作任何集合类型的替代方法。 它提供了访问非并行(缺少)的便捷方法,以及直接覆盖元素并将元素连接到其他元素的能力:​​Streamable​​​​Iterable​​​​Stream​​​​Iterable​​​​….filter(…)​​​​….map(…)​​​​Streamable​

例 40。使用可流式处理合并查询方法结果

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"));
返回自定义可流式传输包装器类型

为集合提供专用包装器类型是一种常用模式,用于为返回多个元素的查询结果提供 API。 通常,通过调用返回类似集合类型的存储库方法并手动创建包装器类型的实例来使用这些类型。 您可以避免该额外步骤,因为如果满足以下条件,Spring Data 允许您将这些包装器类型用作查询方法返回类型:

  1. 类型实现。Streamable
  2. 该类型公开构造函数或名为 dorthat 的静态工厂方法作为参数。of(…)valueOf(…)Streamable

下面的清单显示了一个示例:

class Product {                                         
MonetaryAmount getPrice() { … }
}

@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {

private final Streamable<Product> streamable;

public MonetaryAmount getTotal() {
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}


@Override
public Iterator<Product> iterator() {
return streamable.iterator();
}
}

interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text);
}

公开 API 以访问产品价格的实体。​​Product​

可以使用(使用龙目岛注释创建的工厂方法)构造的包装器类型。 一个标准的构造函数也这样做。​​Streamable<Product>​​​​Products.of(…)​​​​Streamable<Product>​

包装器类型公开一个额外的 API,计算新值。​​Streamable<Product>​

实现接口并委托给实际结果。​​Streamable​

该包装器类型可以直接用作查询方法返回类型。 您无需在存储库客户端中查询后返回并手动包装它。​​Products​​​​Streamable<Product>​

支持 Vavr 集合

Vavr是一个包含Java函数式编程概念的库。 它附带一组可用作查询方法返回类型的自定义集合类型,如下表所示:

Vavr 采集类型

使用的 Vavr 实现类型

有效的 Java 源类型

​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 类型。 或者,您可以声明(Vavr等效),然后我们从实际返回值中派生实现类。 也就是说,ais 变成了 Vavror,变成了 Vavr,依此类推。​​Traversable​​​​Iterable​​​​java.util.List​​​​List​​​​Seq​​​​java.util.Set​​​​LinkedHashSet​​​​Set​