Spring Data(数据)R2DBC

时间:2022-11-22 12:13:53

版本 3.0.0

Spring Data R2DBC项目将Spring的核心概念应用于开发使用关系数据库R2DBC​驱动程序的解决方案。 我们提供了用于存储和查询行的高级抽象。​​DatabaseClient​

本文档是 Spring Data - R2DBC 支持的参考指南。 它解释了R2DBC模块的概念和语义。

本节提供了一些关于 Spring 和数据库的基本介绍。

Spring Data(数据)R2DBC

1. 学习之春

Spring Data 使用 Spring 框架的核心功能,包括:

  • 国际奥委会容器
  • 型式转换系统
  • 表达式语言
  • JMX 集成
  • DAO 异常层次结构。

虽然你不需要知道Spring API,但理解它们背后的概念是很重要的。 至少,控制反转 (IoC) 背后的想法应该很熟悉,并且您应该熟悉您选择使用的任何 IoC 容器。

您可以直接使用 R2DBC 支持的核心功能,而无需调用 Spring 容器的 IoC 服务。 这很像,它可以“独立”使用,而无需 Spring 容器的任何其他服务。 要使用 Spring Data R2DBC 的所有功能,例如存储库支持,您需要将库的某些部分配置为使用 Spring。​​JdbcTemplate​

要了解有关 Spring 的更多信息,请参阅详细解释 Spring 框架的综合文档。 有很多关于这个主题的文章、博客条目和书籍。 有关更多信息,请参阅 Spring 框架主页。

2. 什么是R2DBC?

R2DBC是反应式关系数据库连接的首字母缩写。 R2DBC 是一项 API 规范计划,它声明了一个响应式 API,由驱动程序供应商实现以访问其关系数据库。

关于创建R2DBC的部分答案是需要一个非阻塞应用程序堆栈来处理少量线程的并发性,并以更少的硬件资源进行扩展。 重用标准化的关系数据库访问 API(即 JDBC)无法满足这种需求,因为 JDBC 是一个完全阻塞的 API。 尝试使用有限的使用来补偿阻止行为。​​ThreadPool​

答案的另一部分是大多数应用程序使用关系数据库来存储其数据。 虽然一些NoSQL数据库供应商为其数据库提供了反应式数据库客户端,但对于大多数项目来说,迁移到NoSQL不是一个选项。 这就是新的通用 API 作为任何非阻塞数据库驱动程序的基础的动机。 虽然开源生态系统托管各种非阻塞关系数据库驱动程序实现,但每个客户端都带有特定于供应商的 API,因此不可能在这些库之上建立通用层。

3. 什么是反应式?

术语“反应式”是指围绕对更改、可用性和可处理性做出反应而构建的编程模型 - 网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应,可用的资源等。 从这个意义上说,非阻塞是被动的,因为我们现在不是被阻止,而是在操作完成或数据可用时对通知做出反应。

我们弹簧团队还有另一个重要的机制与反应性有关,那就是非阻塞背压。 在同步的命令性代码中,阻止调用是一种自然形式的背压,迫使调用方等待。 在非阻塞代码中,控制事件速率变得至关重要,这样快速生产者就不会淹没其目标。

Reactive Streams 是一个小规范(在Java 9 中也采用),它定义了异步组件与背压之间的交互。 例如,数据存储库(充当发布服务器)可以生成数据,然后 HTTP 服务器(充当订阅服务器)可以将其写入响应。 反应式流的主要目的是让订阅者控制发布者生成数据的速度或速度。

4. 反应式接口

反应式流在互操作性方面起着重要作用。它对库和基础结构组件感兴趣,但作为应用程序 API 不太有用,因为它太低级了。 应用程序需要一个更高层次、更丰富的功能性 API 来组成异步逻辑 - 类似于 Java 8 Stream API,但不仅适用于表。 这就是响应式库所扮演的角色。

Project Reactor是 Spring Data R2DBC 的首选反应式库。 它提供了单声道和通量API 类型,通过一组与运算符的 ReactiveX 词汇表一致的丰富运算符来处理 () 和 () 的数据序列。 反应器是一个反应流库,因此,它的所有运算符都支持非阻塞背压。 Reactor 非常关注服务器端 Java。它是与Spring密切合作开发的。​​0..1​​​​Mono​​​​0..N​​​​Flux​

Spring Data R2DBC 需要 Project Reactor 作为核心依赖项,但它可以通过 Reactive Streams 规范与其他响应式库互操作。 作为一般规则,Spring Data R2DBC 存储库接受普通输入,在内部将其适应 Reactor 类型,使用该类型,并返回 aor aas 输出。 因此,您可以传递 anyas 输入并对输出应用操作,但您需要调整输出以用于另一个反应式库。 只要可行,Spring Data就会透明地适应RxJava或其他反应式库的使用。​​Publisher​​​​Mono​​​​Flux​​​​Publisher​

5. 要求

Spring Data R2DBC 3.x二进制文件需要:

  • JDK 17 级及以上
  • Spring 框架6.0.0 及更高版本
  • R2DBC及以上

6. 其他帮助资源

学习一个新框架并不总是那么简单。 在本节中,我们尝试提供我们认为易于遵循的指南,以便从Spring Data R2DBC模块开始。 但是,如果您遇到问题或需要建议,请使用以下链接之一:

社区论坛

Stack Overflow上的Spring Data是所有Spring Data(不仅仅是R2DBC)用户共享信息和互相帮助的标签。 请注意,只有发布时才需要注册。

专业支持

专业的,从源头支持,保证响应时间,可从Spring Data和Spring背后的公司Pivotal Software,Inc.获得。

7. 后续开发

  • 有关 Spring Data R2DBC 源代码存储库、夜间构建和快照工件的信息,请参阅 Spring Data R2DBC主页。
  • 您可以通过Stack Overflow上的社区与开发人员进行交互,从而帮助Spring Data最好地满足Spring社区的需求。
  • 如果您遇到错误或想要提出改进建议,请在Spring Data R2DBC问题跟踪器上创建一个票证。
  • 要及时了解 Spring 生态系统中的最新消息和公告,请订阅 Spring 社区门户。
  • 您也可以在Twitter(SpringData)上关注Spring博客或Spring Data项目团队。

8. 项目元数据

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

9. 升级弹簧数据

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

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

10. 依赖关系

由于各个 Spring 数据模块的开始日期不同,因此它们中的大多数都带有不同的主要和次要版本号。找到兼容版本的最简单方法是依靠我们随附的与定义的兼容版本一起提供的春季数据发布列车 BOM。在 Maven 项目中,您将在 POM 的部分中声明此依赖项,如下所示:​​<dependencyManagement />​

例 1.使用弹簧数据发布列车物料清单

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2022.0.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

当前发布训练版本是。火车版本使用带有图案的犊牛。 对于 GA 版本和服务版本,版本名称如下,对于所有其他版本,版本名称如下:,其中可以是以下之一:​​2022.0.0​​​​YYYY.MINOR.MICRO​​​​${calver}​​​​${calver}-${modifier}​​​​modifier​

  • ​SNAPSHOT​​:当前快照
  • ​M1​​,,等等:里程碑M2
  • ​RC1​​,,等等:发布候选版本RC2

您可以在我们的Spring 数据示例存储库中找到使用 BOM 的工作示例。有了这个,你可以声明你想要使用的 Spring 数据模块,而无需在块中有一个版本,如下所示:​​<dependencies />​

例 2.声明对 Spring 数据模块的依赖关系

<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>

10.1. 使用 Spring 引导进行依赖管理

Spring Boot 会为你选择最新版本的 Spring 数据模块。如果仍要升级到较新版本,请将 要使用的训练版本和迭代的属性。​​spring-data-releasetrain.version​

10.2. 弹簧框架

Spring 数据模块的当前版本需要 Spring Framework 6.0.0 或更高版本。这些模块还可以使用该次要版本的较旧错误修复版本。但是,强烈建议使用该代中的最新版本。

11. 使用 Spring 数据存储库

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


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



本章解释了 Spring 数据存储库的核心概念和接口。 本章中的信息来自 Spring 数据共享模块。 它使用 Jakarta 持久性 API (JPA) 模块的配置和代码示例。 “存储库查询关键字”涵盖了存储库抽象通常支持的查询方法关键字。 有关模块特定功能的详细信息,请参阅本文档有关该模块的章节。


11.1. 核心概念

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

示例 3.界面​​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​

例 4.接口​​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));

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

例 5.派生计数查询

interface UserRepository extends CrudRepository<User, Long> {

long countByLastname(String lastname);
}

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

例 6.派生删除查询

interface UserRepository extends CrudRepository<User, Long> {

long deleteByLastname(String lastname);

List<User> removeByLastname(String lastname);
}

11.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 配置为这些接口创建代理实例。

    爪哇岛
@EnableJpaRepositories
class Config { … }

+ 请注意,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 数据存储库的自定义实现

11.3. 定义存储库接口

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

11.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​

例 7.有选择地公开 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​

11.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)的存储库:

例 8.使用特定于模块的接口的存储库定义

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​

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

例 9.使用通用接口的存储库定义

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​

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

例 10.使用带有注释的域类的存储库定义

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​

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

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

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 的配置中的基本包是必需的。

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

例 12.注释驱动的基本包配置

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

11.4. 定义查询方法

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

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

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

11.4.1. 查询查找策略

存储库基础结构可以使用以下策略来解析查询。 对于 Java 配置,您可以使用注释的属性。 特定数据存储可能不支持某些策略。​​queryLookupStrategy​​​​EnableJpaRepositories​

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

11.4.2. 查询创建

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

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

例 13.从方法名称创建查询

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

11.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 命名约定(即,不要在属性名称中使用下划线,而是使用驼峰大小写)。

11.4.4. 特殊参数处理

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

例 14。使用 、 和 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​

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

分页和排序

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

例 15。定义排序表达式

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

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

例 16。使用类型安全的 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,您还可以使用生成的元模型类型来定义排序表达式:

例 17.使用 Querydsl API 定义排序表达式

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

11.4.5. 限制查询结果

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

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

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

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

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

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

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

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​

11.4.7. 存储库方法的空处理

从Spring Data 2.0开始,返回单个聚合实例的存储库CRUD方法使用Java 8来指示可能缺少值。 除此之外,Spring Data 还支持在查询方法上返回以下包装器类型:​​Optional​

  • ​com.google.common.base.Optional​
  • ​scala.Option​
  • ​io.vavr.control.Option​

或者,查询方法可以选择根本不使用包装器类型。 然后通过返回来指示缺少查询结果。 返回集合、集合替代项、包装器和流的存储库方法保证永远不会返回,而是返回相应的空表示形式。 有关详细信息,请参阅 “存储库查询返回类型”。nullnull

可为空性注释

您可以使用 Spring Framework 的可空性注释来表达存储库方法的可空性约束。 它们提供了一种工具友好的方法,并在运行时进行选择加入,如下所示:null

  • @NonNullApi:在包级别用于声明参数和返回值的默认行为分别是既不接受也不生成值。null
  • @NonNull:用于不得使用的参数或返回值(在适用的参数和返回值上不需要)。null@NonNullApi
  • @Nullable:用于可以的参数或返回值。null

Spring 注释是用 JSR305注释(一种休眠但广泛使用的 JSR)进行元注释的。 JSR 305元注解允许工具供应商(如IDEA,Eclipse和Kotlin)以通用方式提供空安全支持,而不必对Spring注解进行硬编码支持。 要启用查询方法的可空性约束的运行时检查,您需要使用 Spring'sin在包级别激活非可空性,如以下示例所示:​​@NonNullApi​​​​package-info.java​

例 20。声明 中的非可空性​​package-info.java​

@org.springframework.lang.NonNullApi
package com.acme;

一旦非 null 默认值到位,存储库查询方法调用将在运行时验证可空性约束。 如果查询结果违反定义的约束,则会引发异常。 当方法将返回但被声明为不可为空(默认值,在存储库所在的包上定义的注释)时,会发生这种情况。 如果要再次选择加入可为空的结果,请有选择地使用单个方法。 使用本节开头提到的结果包装器类型将继续按预期工作:空结果将转换为表示缺席的值。​​null​​​​@Nullable​

以下示例显示了刚才描述的许多技术:

例 21。使用不同的可为空性约束

package com.acme;                                                       

interface UserRepository extends Repository<User, Long> {

User getByEmailAddress(EmailAddress emailAddress);

@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress);

Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress);
}

存储库驻留在我们为其定义了非空行为的包(或子包)中。

当查询未生成结果时引发。 扔一个当手到方法就是。​​EmptyResultDataAccessException​​​​IllegalArgumentException​​​​emailAddress​​​​null​

当查询未生成结果时返回。 也接受作为值。​​null​​​​null​​​​emailAddress​

当查询未生成结果时返回。 扔一个当手到方法就是。​​Optional.empty()​​​​IllegalArgumentException​​​​emailAddress​​​​null​

基于 Kotlin 的存储库中的可空性

Kotlin 将可空性约束的定义融入到语言中。 Kotlin 代码编译为字节码,字节码不通过方法签名表示可空性约束,而是通过编译的元数据来表达可空性约束。 确保在您的项目中包含 JAR,以便能够内省 Kotlin 的可空性约束。 Spring 数据存储库使用语言机制来定义这些约束以应用相同的运行时检查,如下所示:​​kotlin-reflect​

例 22。在 Kotlin 存储库上使用可空性约束

interface UserRepository : Repository<User, String> {

fun findByUsername(username: String): User

fun findByFirstname(firstname: String?): User?
}

该方法将参数和结果定义为不可为空(Kotlin 默认值)。 Kotlin 编译器拒绝传递给该方法的方法调用。 如果查询产生空结果,则引发 anis 。​​null​​​​EmptyResultDataAccessException​

此方法接受参数并返回查询未产生结果。​​null​​​​firstname​​​​null​

11.4.8. 流式查询结果

您可以使用 Java 8 作为返回类型以增量方式处理查询方法的结果。 不是将查询结果包装在 中,而是使用特定于数据存储的方法执行流式处理,如以下示例所示:​​Stream<T>​​​​Stream​

例 23。使用 Java 8 流式传输查询结果​​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);

可能会包装特定于基础数据存储的资源,因此必须在使用后关闭。 您可以使用该方法或使用 Java 7block 手动关闭,如以下示例所示:​​Stream​​​​Stream​​​​close()​​​​try-with-resources​

例 24。在块中使用结果​​Stream<T>​​​​try-with-resources​

try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}

并非所有 Spring 数据模块当前都支持返回类型。​​Stream<T>​

11.4.9. 异步查询结果

您可以使用Spring 的异步方法运行功能异步运行存储库查询。 这意味着该方法在调用时立即返回,而实际查询发生在已提交给 Spring 的任务中。 异步查询不同于反应式查询,不应混合使用。 有关反应式支持的更多详细信息,请参阅特定于商店的文档。 以下示例显示了许多异步查询:​​TaskExecutor​

@Async
Future<User> findByFirstname(String firstname);

@Async
CompletableFuture<User> findOneByFirstname(String firstname);

用作返回类型。​​java.util.concurrent.Future​

使用 Java 8 作为返回类型。​​java.util.concurrent.CompletableFuture​

Spring Data(数据)R2DBC

11.5. 创建存储库实例

本节介绍如何为定义的存储库接口创建实例和 Bean 定义。

11.5.1. Java 配置

使用 Java 配置类上的特定于存储的注释来定义存储库激活的配置。 有关 Spring 容器的基于 Java 的配置的介绍,请参阅Spring 参考文档中的 JavaConfig。​​@EnableJpaRepositories​

启用 Spring 数据存储库的示例配置类似于以下内容:

例 25。基于注释的存储库配置示例

@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {

@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}

前面的示例使用特定于 JPA 的注释,您将根据实际使用的存储模块对其进行更改。这同样适用于thebean的定义。请参阅涵盖特定于商店的配置的部分。​​EntityManagerFactory​

11.5.2. 使用过滤器

缺省情况下,基础结构选取扩展位于配置的基础包下的特定于持久性技术的子接口的每个接口,并为其创建一个 Bean 实例。 但是,您可能希望更精细地控制哪些接口为其创建了 Bean 实例。 为此,请在存储库声明中使用过滤器元素。 语义与 Spring 组件过滤器中的元素完全相同。 有关详细信息,请参阅这些元素的Spring 参考文档。​​Repository​

例如,要从实例化中排除某些接口作为存储库 Bean,您可以使用以下配置:

例 26。使用过滤器

爪哇岛

@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() {
// …
}
}

前面的示例排除了以实例化结尾的所有接口,并包括以 结尾的接口。​​SomeRepository​​​​SomeOtherRepository​

11.5.3. 独立使用

您还可以在 Spring 容器之外使用存储库基础架构,例如,在 CDI 环境中。你的类路径中仍然需要一些 Spring 库,但通常,你也可以以编程方式设置存储库。提供存储库支持的 Spring 数据模块附带了您可以使用的特定于持久性技术的模块,如下所示:​​RepositoryFactory​

例 27。存储库工厂的独立使用

RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);

11.6. Spring 数据存储库的自定义实现

Spring Data 提供了各种选项来创建查询方法,只需很少的编码。 但是,当这些选项不符合您的需求时,您还可以为存储库方法提供自己的自定义实现。 本节介绍如何执行此操作。

11.6.1. 自定义单个仓库

要使用自定义功能丰富存储库,您必须首先定义片段接口和自定义功能的实现,如下所示:

例 28。自定义存储库功能的界面

interface CustomizedUserRepository {
void someCustomMethod(User user);
}

例 29。实现自定义存储库功能

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

public void someCustomMethod(User user) {
// Your custom implementation
}
}

与片段接口对应的类名中最重要的部分是后缀。​​Impl​

实现本身不依赖于 Spring 数据,可以是常规的 Spring bean。 因此,您可以使用标准的依赖注入行为来注入对其他 Bean 的引用(例如 a)、参与方面等。​​JdbcTemplate​

然后,您可以让存储库接口扩展片段接口,如下所示:

例 30。对存储库界面的更改

interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

// Declare query methods here
}

使用存储库接口扩展片段接口结合了 CRUD 和自定义功能,并使其可供客户端使用。

Spring 数据存储库是通过使用形成存储库组合的片段来实现的。 片段是基本存储库、功能方面(如QueryDsl)和自定义接口及其实现。 每次向存储库界面添加接口时,都会通过添加片段来增强组合。 基本存储库和存储库方面实现由每个 Spring 数据模块提供。

以下示例显示了自定义接口及其实现:

例 31。片段及其实现

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​

例 32。对存储库界面的更改

interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

// Declare query methods here
}

存储库可以由多个自定义实现组成,这些实现按其声明顺序导入。 自定义实现的优先级高于基本实现和存储库方面。 此排序允许您覆盖基本存储库和方面方法,并在两个片段提供相同的方法签名时解决歧义。 存储库片段不限于在单个存储库界面中使用。 多个存储库可以使用片段界面,允许您跨不同存储库重用自定义项。

以下示例显示了存储库片段及其实现:

例 33。片段覆盖​​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
}
}

以下示例显示了使用上述存储库片段的存储库:

例 34。自定义存储库接口

interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
配置

存储库基础结构尝试通过扫描找到存储库的包下的类来自动检测自定义实现片段。 这些类需要遵循附加默认后缀的命名约定。​​Impl​

以下示例显示了一个使用默认后缀的存储库和一个为后缀设置自定义值的存储库:

例 35。配置示例

爪哇岛

@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }

前面示例中的第一个配置尝试查找调用充当自定义存储库实现的类。 第二个示例尝试查找。​​com.acme.repository.CustomizedUserRepositoryImpl​​​​com.acme.repository.CustomizedUserRepositoryMyPostfix​

歧义的解决

如果在不同的包中找到具有匹配类名的多个实现,Spring Data 将使用 bean 名称来标识要使用的实现。

给定前面所示的以下两个自定义实现,将使用第一个实现。 它的 Bean 名称是,与片段接口 () 加上后缀的名称相匹配。​​CustomizedUserRepository​​​​customizedUserRepositoryImpl​​​​CustomizedUserRepository​​​​Impl​

例 36。解决不明确的实现

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
}

如果您注释接口,则 bean 名称 plusthen 与为存储库实现定义的名称匹配,并且使用它代替第一个。​​UserRepository​​​​@Component("specialCustom")​​​​Impl​​​​com.acme.impl.two​

手动接线

如果您的自定义实现仅使用基于注释的配置和自动连线,则上述方法效果很好,因为它被视为任何其他 Spring Bean。 如果您的实现片段 Bean 需要特殊连接,则可以声明 Bean 并根据上一节中描述的约定对其进行命名。 然后,基础结构按名称引用手动定义的 Bean 定义,而不是自己创建一个。 以下示例演示如何手动连接自定义实现:

例 37。自定义实现的手动接线

爪哇岛

class MyClass {
MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {

}
}

11.6.2. 自定义基础存储库

当您想要自定义基本存储库行为以使所有存储库都受到影响时,上一节中描述的方法需要自定义每个存储库接口。 要改为更改所有存储库的行为,您可以创建一个实现来扩展特定于持久性技术的存储库基类。 然后,此类充当存储库代理的自定义基类,如以下示例所示:

例 38。自定义存储库基类

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

该类需要具有特定于存储的存储库工厂实现使用的超类的构造函数。 如果存储库基类有多个构造函数,请覆盖采用存储特定基础结构对象(例如模板类)的构造函数。​​EntityInformation​​​​EntityManager​

最后一步是使 Spring 数据基础架构知道自定义的存储库基类。 在配置中,可以使用 来执行此操作,如以下示例所示:​​repositoryBaseClass​

例 39。配置自定义存储库基类

爪哇岛

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }

11.7. 从聚合根发布事件

存储库管理的实体是聚合根。 在域驱动设计应用程序中,这些聚合根通常发布域事件。 Spring Data 提供了一个注释,称为您可以在聚合根的方法上使用该注释,以使该发布尽可能简单,如以下示例所示:​​@DomainEvents​

例 40。从聚合根公开域事件

class AnAggregateRoot {

@DomainEvents
Collection<Object> domainEvents() {
// … return events you want to get published here
}

@AfterDomainEventPublication
void callbackMethod() {
// … potentially clean up domain events list
}
}

使用的方法可以返回单个事件实例或事件集合。 它绝不能接受任何论据。​​@DomainEvents​

发布所有事件后,我们有一个注释方法。 您可以使用它来潜在地清理要发布的事件列表(以及其他用途)。​​@AfterDomainEventPublication​

每次调用 Spring 数据存储库或方法之一时都会调用这些方法。​​save(…)​​​​saveAll(…)​​​​delete(…)​​​​deleteAll(…)​

11.8. 弹簧数据扩展

本节记录了一组 Spring 数据扩展,这些扩展允许在各种上下文中使用 Spring 数据。 目前,大多数集成都是针对Spring MVC的。

11.8.1. 查询扩展

Querydsl是一个框架,它支持通过其流畅的API构造静态类型的类似SQL的查询。

几个 Spring 数据模块提供与 Querydsl 的集成,如以下示例所示:​​QuerydslPredicateExecutor​

例 41。QuerydslPredicateExecutor interface

public interface QuerydslPredicateExecutor<T> {

Optional<T> findById(Predicate predicate);

Iterable<T> findAll(Predicate predicate);

long count(Predicate predicate);

boolean exists(Predicate predicate);

// … more functionality omitted.
}

查找并返回与 匹配的单个实体。​​Predicate​

查找并返回与 匹配的所有实体。​​Predicate​

返回与 匹配的实体数。​​Predicate​

返回与实体匹配的实体是否存在。​​Predicate​

要使用 Querydsl 支持,请扩展存储库接口,如以下示例所示:​​QuerydslPredicateExecutor​

例 42。存储库上的 querydsl 集成

interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

前面的示例允许您使用 Querydslinstances 编写类型安全的查询,如以下示例所示:​​Predicate​

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

11.8.2. 网站支持

支持存储库编程模型的 Spring 数据模块附带了各种 Web 支持。 与Web相关的组件要求Spring MVC JAR位于类路径上。 其中一些甚至提供与Spring HATEOAS的集成。 通常,集成支持是通过在 JavaConfig 配置类中使用注释来启用的,如以下示例所示:​​@EnableSpringDataWebSupport​

例 43。启用 Spring 数据网络支持

爪哇岛

.XML

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}

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

基本网络支持

在XML中启用Spring Data Web支持

上一节中显示的配置注册了一些基本组件:

  • A使用DomainClassConverter类让Spring MVC 从请求参数或路径变量解析存储库管理的域类的实例。
  • HandlerMethodArgumentResolver实现,让 Spring MVC 从请求参数解析和实例。PageableSort
  • 杰克逊模块,用于反序列化类型,或存储特定的类型,具体取决于所使用的 Spring 数据模块。PointDistance
使用类​​DomainClassConverter​

该类允许您直接在Spring MVC控制器方法签名中使用域类型,这样您就不需要通过存储库手动查找实例,如以下示例所示:​​DomainClassConverter​

例 44。在方法签名中使用域类型的Spring MVC控制器

@Controller
@RequestMapping("/users")
class UserController {

@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {

model.addAttribute("user", user);
return "userForm";
}
}

该方法直接接收实例,无需进一步查找。 可以通过让Spring MVC首先将路径变量转换为域类的类型,并最终通过调用为域类型注册的存储库实例来访问实例来解决该实例。​​User​​​​id​​​​findById(…)​

目前,存储库必须实现才有资格被发现进行转换。​​CrudRepository​

用于可分页和排序的处理程序方法参数解析器

上一节中显示的配置片段还注册了 aas 以及 的实例。 注册启用 sandas 有效控制器方法参数,如以下示例所示:​​PageableHandlerMethodArgumentResolver​​​​SortHandlerMethodArgumentResolver​​​​Pageable​​​​Sort​

例 45。使用可分页作为控制器方法参数

@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​

表 1.为实例评估的请求参数​​Pageable​

​page​

要检索的页面。0 索引,默认为 0。

​size​

要检索的页面的大小。默认值为 20。

​sort​

应按格式排序的属性。默认排序方向为区分大小写的升序。如果要切换方向或区分大小写,请使用多个参数,例如。​​property,property(,ASC|DESC)(,IgnoreCase)​​​​sort​​​​?sort=firstname&sort=lastname,asc&sort=city,ignorecase​

要定制此行为,请分别注册实现接口或接口的 Bean。 调用 Itsmethod,允许您更改设置,如以下示例所示:​​PageableHandlerMethodArgumentResolverCustomizer​​​​SortHandlerMethodArgumentResolverCustomizer​​​​customize()​

@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}

如果设置现有属性不足以满足您的目的,请扩展启用 HATEOAS 的等效项,覆盖理论方法,并导入自定义配置文件,而不是使用注释。​​MethodArgumentResolver​​​​SpringDataWebConfiguration​​​​pageableResolver()​​​​sortResolver()​​​​@Enable​

如果您需要从请求中解析多个实例(例如,对于多个表),则可以使用 Spring'sannotation 来区分彼此。 然后,请求参数必须带有前缀。 下面的示例演示生成的方法签名:​​Pageable​​​​Sort​​​​@Qualifier​​​​${qualifier}_​

String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }

您必须填充,等等。​​thing1_page​​​​thing2_page​

默认传递给方法等效于 a,但您可以通过对参数使用注释来自定义它。​​Pageable​​​​PageRequest.of(0, 20)​​​​@PageableDefault​​​​Pageable​

对可分页的超媒体支持

Spring HATEOAS 附带了一个表示模型类 (),它允许使用必要的元数据和链接来丰富实例的内容,让客户端轻松浏览页面。 ato ais 的转换是通过 Spring HATEOAS接口的实现完成的,称为。 下面的示例演示如何使用 aas 控制器方法参数:​​PagedResources​​​​Page​​​​Page​​​​Page​​​​PagedResources​​​​ResourceAssembler​​​​PagedResourcesAssembler​​​​PagedResourcesAssembler​

例 46。使用 PagedResourcesAssembler 作为控制器方法参数

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

启用配置(如前面的示例所示)允许将 thebe 用作控制器方法参数。 呼叫它具有以下效果:​​PagedResourcesAssembler​​​​toResources(…)​

  • 的内容成为实例的内容。PagePagedResources
  • 对象被附加一个实例,并填充来自底层的信息。PagedResourcesPageMetadataPagePageRequest
  • 附上可能获取链接,具体取决于页面的状态。 链接指向方法映射到的 URI。 添加到方法的分页参数与设置匹配,以确保以后可以解析链接。PagedResourcesprevnextPageableHandlerMethodArgumentResolver

假设数据库中有 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
}
}

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

弹簧数据杰克逊模块

核心模块,以及一些特定于商店的模块,附带了一组杰克逊模块,用于Spring Data域使用的类似类型。
一旦启用了Web 支持并且可用,这些模块就会被导入。​​org.springframework.data.geo.Distance​​​​org.springframework.data.geo.Point​​​​com.fasterxml.jackson.databind.ObjectMapper​

在初始化期间,就像 一样,由基础结构拾取,以便声明的 Jackson 可用。​​SpringDataJacksonModules​​​​SpringDataJacksonConfiguration​​​​com.fasterxml.jackson.databind.Module​​​​ObjectMapper​

以下域类型的数据绑定混合由通用基础结构注册。

org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon


单个模块可以提供额外的。
有关更多详细信息,请参阅商店特定部分。​​​SpringDataJacksonModules​


网页数据绑定支持

您可以使用 Spring 数据投影(在投影中描述)通过使用 JSONPath 表达式(需要Jayway JsonPath)或XPath表达式(需要XmlBeam)来绑定传入的请求有效负载,如以下示例所示:

例 47。使用 JSONPath 或 XPath 表达式的 HTTP 有效负载绑定

@ProjectedPayload
public interface UserPayload {

@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();

@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}

您可以使用前面示例中所示的类型作为 Spring MVC 处理程序方法参数,也可以使用 on 的方法之一。 前面的方法声明将尝试在给定文档中的任何地方查找。 XML 查找在传入文档的顶层执行。 它的 JSON 变体尝试*优先,但如果前者不返回值,也会尝试嵌套在子文档中。 这样,可以轻松缓解源文档结构中的更改,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。​​ParameterizedTypeReference​​​​RestTemplate​​​​firstname​​​​lastname​​​​lastname​​​​lastname​​​​user​

支持嵌套投影,如投影中所述。 如果该方法返回复杂的非接口类型,则使用 Jacksonis 映射最终值。​​ObjectMapper​

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

有关更多信息,请参阅规范的 Spring 数据示例存储库中的Web 投影示例。

查询网络支持

对于那些具有QueryDSL集成的存储,可以从查询字符串中包含的属性派生查询。​​Request​

请考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

给定前面示例中的对象,可以使用 将查询字符串解析为以下值,如下所示:​​User​​​​QuerydslPredicateArgumentResolver​

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))

当在类路径上找到 Querydsl 时,将自动启用该功能。​​@EnableSpringDataWebSupport​

将 ato 添加到方法签名提供了一个即用型,您可以使用 来运行。​​@QuerydslPredicate​​​​Predicate​​​​QuerydslPredicateExecutor​

类型信息通常从方法的返回类型中解析。 由于该信息不一定与域类型匹配,因此最好使用 的属性。​​root​​​​QuerydslPredicate​

下面的示例演示如何在方法中使用签名:​​@QuerydslPredicate​

@Controller
class UserController {

@Autowired UserRepository repository;

@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {

model.addAttribute("users", repository.findAll(predicate, pageable));

return "index";
}
}

将查询字符串参数解析为匹配对象。​​Predicate​​​​User​

默认绑定如下所示:

  • ​Object​​在简单属性上。eq
  • ​Object​​在集合上像属性一样。contains
  • ​Collection​​在简单属性上。in

您可以通过属性 ofor 自定义这些绑定,方法是使用 Java 8 并将方法添加到存储库接口,如下所示:​​bindings​​​​@QuerydslPredicate​​​​default methods​​​​QuerydslBinderCustomizer​

interface UserRepository extends CrudRepository<User, String>,
QuerydslPredicateExecutor<User>,
QuerydslBinderCustomizer<QUser> {

@Override
default void customize(QuerydslBindings bindings, QUser user) {

bindings.bind(user.username).first((path, value) -> path.contains(value))
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value));
bindings.excluding(user.password);
}
}

​QuerydslPredicateExecutor​​​提供对特定查找器方法的访问。​​Predicate​

​QuerydslBinderCustomizer​​​在存储库界面上定义的自动拾取和快捷方式。​​@QuerydslPredicate(bindings=…)​

将属性的绑定定义为简单绑定。​​username​​​​contains​

将属性的默认绑定定义为不区分大小写的匹配项。​​String​​​​contains​

从解析中排除该属性。​​password​​​​Predicate​

您可以在从存储库或应用特定绑定之前注册持有默认 Querydsl 绑定的 abean。​​QuerydslBinderCustomizerDefaults​​​​@QuerydslPredicate​

12. 简介

12.1. 文档结构

参考文档的这一部分解释了Spring Data R2DBC提供的核心功能。

“R2DBC 支持”引入了 R2DBC 模块功能集。

“R2DBC 存储库”介绍了对 R2DBC 的存储库支持。

13. R2DBC支持

R2DBC包含广泛的功能:

  • Spring 配置支持基于 Java 的 R2DBC 驱动程序实例的类。@Configuration
  • ​R2dbcEntityTemplate​​作为实体绑定操作的中心类,在执行常见 R2DBC 操作时通过行和 POJO 之间的集成对象映射提高工作效率。
  • 功能丰富的对象映射与 Spring 的转换服务集成。
  • 可扩展以支持其他元数据格式的基于批注的映射元数据。
  • 自动实现存储库接口,包括对自定义查询方法的支持。

对于大多数任务,您应该使用存储库支持,它都使用丰富的映射 functionality.is 查找访问功能(如临时 CRUD 操作)的位置。​​R2dbcEntityTemplate​​​​R2dbcEntityTemplate​

13.1. 入门

设置工作环境的一种简单方法是通过start.spring.io 创建一个基于 Spring 的项目。 为此:

  1. 将以下内容添加到 pom.xml 文件元素:​​dependencies​
<dependencies>

<!-- other dependency elements omitted -->

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-r2dbc</artifactId>
<version>3.0.0</version>
</dependency>

<!-- a R2DBC driver -->
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<version>x.y.z</version>
</dependency>

</dependencies>
  1. 将 pom 中的 Spring 版本.xml更改为
<spring-framework.version>6.0.0</spring-framework.version>
  1. 将 Maven 的 Spring 里程碑存储库的以下位置添加到您的位置,使其与您的元素处于同一级别:pom.xml<dependencies/>


<repositories>
<repository>
<id>spring-milestone</id>
<name>Spring Maven MILESTONE Repository</name>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
</repositories>

存储库也可以在此处浏览。

您可能还希望将日志记录级别设置为 以查看一些其他信息。 为此,请编辑文件以包含以下内容:​​DEBUG​​​​application.properties​

logging.level.org.springframework.r2dbc=DEBUG

然后,例如,您可以创建 aclass 来持久化,如下所示:​​Person​

public class Person {

private final String id;
private final String name;
private final int age;

public Person(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}

public String getId() {
return id;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}

接下来,您需要在数据库中创建一个表结构,如下所示:

CREATE TABLE person
(id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
age INT);

您还需要一个主应用程序才能运行,如下所示:

public class R2dbcApp {

private static final Log log = LogFactory.getLog(R2dbcApp.class);

public static void main(String[] args) {

ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");

R2dbcEntityTemplate template = new R2dbcEntityTemplate(connectionFactory);

template.getDatabaseClient().sql("CREATE TABLE person" +
"(id VARCHAR(255) PRIMARY KEY," +
"name VARCHAR(255)," +
"age INT)")
.fetch()
.rowsUpdated()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();

template.insert(Person.class)
.using(new Person("joe", "Joe", 34))
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();

template.select(Person.class)
.first()
.doOnNext(it -> log.info(it))
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}

运行主程序时,前面的示例将生成类似于以下内容的输出:

2018-11-28 10:47:03,893 DEBUG amework.core.r2dbc.DefaultDatabaseClient: 310 - Executing SQL statement [CREATE TABLE person
(id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
age INT)]
2018-11-28 10:47:04,074 DEBUG amework.core.r2dbc.DefaultDatabaseClient: 908 - Executing SQL statement [INSERT INTO person (id, name, age) VALUES($1, $2, $3)]
2018-11-28 10:47:04,092 DEBUG amework.core.r2dbc.DefaultDatabaseClient: 575 - Executing SQL statement [SELECT id, name, age FROM person]
2018-11-28 10:47:04,436 INFO org.spring.r2dbc.example.R2dbcApp: 43 - Person [id='joe', name='Joe', age=34]

即使在这个简单的示例中,也很少有需要注意的事项:

  • 您可以使用标准对象在 Spring Data R2DBC () 中创建中心辅助类的实例。R2dbcEntityTemplateio.r2dbc.spi.ConnectionFactory
  • 映射器针对标准 POJO 对象工作,而无需任何其他元数据(尽管您可以选择提供该信息 — 请参阅此处)。
  • 映射约定可以使用字段访问。请注意,该类只有 getter。Person
  • 如果构造函数参数名称与存储行的列名称匹配,则它们用于实例化对象。

13.2. 示例存储库

有一个GitHub 存储库,其中包含几个示例,您可以下载并试用这些示例,以了解库的工作原理。

13.3. 使用 Spring 连接到关系数据库

使用关系数据库和 Spring 时的首要任务之一是使用 IoC 容器创建对象。请确保使用受支持的数据库和驱动程序。​​io.r2dbc.spi.ConnectionFactory​

13.3.1. 使用基于 Java 的元数据注册实例​​ConnectionFactory​

以下示例显示了使用基于 Java 的 Bean 元数据注册 的实例的示例:​​io.r2dbc.spi.ConnectionFactory​

例 48。使用基于 Java 的 Bean 元数据注册对象​​io.r2dbc.spi.ConnectionFactory​

@Configuration
public class ApplicationConfiguration extends AbstractR2dbcConfiguration {

@Override
@Bean
public ConnectionFactory connectionFactory() {
return …
}
}

这种方法允许您使用标准实例,容器使用 Spring 的。与直接注册实例相比,配置支持还有一个额外的优势,即还为容器提供了一个实现,该实现将 R2DBC 异常转换为 Spring 的可移植层次结构中的异常,用于使用注释注释的数据访问类。这种层次结构和用法在Spring 的 DAO 支持功能中进行了描述。​​io.r2dbc.spi.ConnectionFactory​​​​AbstractR2dbcConfiguration​​​​ConnectionFactory​​​​ExceptionTranslator​​​​DataAccessException​​​​@Repository​​​​@Repository​

​AbstractR2dbcConfiguration​​也是寄存器,这是数据库交互和存储库实现所必需的。​​DatabaseClient​

13.3.2. R2DBC 驱动程序

Spring Data R2DBC通过R2DBC的可插拔SPI机制支持驱动程序。 您可以将任何实现 R2DBC 规范的驱动程序与 Spring Data R2DBC 一起使用。 由于Spring Data R2DBC对每个数据库的特定功能做出反应,因此它需要一个实现,否则您的应用程序将无法启动。 Spring Data R2DBC 附带了以下驱动程序的方言实现:​​Dialect​

  • H2 (io.r2dbc:r2dbc-h2)
  • 玛丽亚数据库 (org.mariadb:r2dbc-mariadb)
  • 微软SQL Server (io.r2dbc:r2dbc-mssql)
  • jasync-sql MySQL (com.github.jasync-sql:jasync-r2dbc-mysql)
  • 波斯特格雷斯 (io.r2dbc:r2dbc-postgresql)
  • 神谕 (com.oracle.database.r2dbc:oracle-r2dbc)

Spring Data R2DBC通过检查数据库细节来对数据库细节做出反应,并相应地选择合适的数据库方言。 您需要配置自己的R2dbcDialect,如果您使用的驱动程序还不知道 Spring Data R2DBC。​​ConnectionFactory​

方言由方言解析器​从 a 解析,通常通过检查。 + 你可以让 Spring 自动发现你的,注册一个类来实现 through.discovery 方言提供者的实现,使用 Spring 的。​​ConnectionFactory​​​​ConnectionFactoryMetadata​​​​R2dbcDialect​​​​org.springframework.data.r2dbc.dialect.DialectResolver$R2dbcDialectProvider​​​​META-INF/spring.factories​​​​DialectResolver​​​​SpringFactoriesLoader​

13.4. R2dbc实体操作数据访问API

​R2dbcEntityTemplate​​是Spring Data R2DBC的中心入口点。 它为典型的临时用例(如查询、插入、更新和删除数据)提供了直接面向实体的方法和更窄、更流畅的界面。

入口点(,,,和其他)遵循基于要运行的操作的自然命名架构。 从入口点继续,API 旨在仅提供依赖于上下文的方法,这些方法会导致创建和运行 SQL 语句的终止方法。 Spring Data R2DBC使用抽象来确定绑定标记,分页支持和底层驱动程序本机支持的数据类型。​​insert()​​​​select()​​​​update()​​​​R2dbcDialect​

所有终端方法始终返回表示所需操作的 atype。 实际语句在订阅时发送到数据库。​​Publisher​

13.4.1. 插入和更新实体的方法

有几种方便的方法用于保存和插入对象。 为了对转换过程进行更精细的控制,您可以注册 Spring 转换器 - 例如。​​R2dbcEntityTemplate​​​​R2dbcCustomConversions​​​​Converter<Person, OutboundRow>​​​​Converter<Row, Person>​

使用保存操作的简单情况是保存 POJO。在这种情况下,表名由类的名称(不完全限定)确定。 您还可以使用特定的集合名称调用保存操作。 可以使用映射元数据来覆盖要在其中存储对象的集合。

插入或保存时,如果未设置属性,则假定其值将由数据库自动生成。 因此,对于自动生成,类中的属性或字段的类型必须是 a,or。​​Id​​​​Id​​​​Long​​​​Integer​

下面的示例演示如何插入行并检索其内容:

例 49。使用​​R2dbcEntityTemplate​

Person person = new Person("John", "Doe");

Mono<Person> saved = template.insert(person);
Mono<Person> loaded = template.selectOne(query(where("firstname").is("John")),
Person.class);

可以使用以下插入和更新操作:

还提供了一组类似的插入操作:

  • ​Mono<T>​​ 插入:将对象插入到默认表格。(T objectToSave)
  • ​Mono<T>​​ update:将对象插入到默认表中。(T objectToSave)

可以使用流畅的 API 自定义表名称。

13.4.2. 选择数据

方法用于从表中选择数据。 这两种方法都采用一个Query对象,该对象定义字段投影、子句、子句和限制/偏移分页。 限制/偏移功能对应用程序是透明的,无论基础数据库如何。 R2dbcDialect抽象支持此功能,以满足各个 SQL 风格之间的差异。​​select(…)​​​​selectOne(…)​​​​R2dbcEntityTemplate​​​​WHERE​​​​ORDER BY​

例 50。使用​​R2dbcEntityTemplate​

Flux<Person> loaded = template.select(query(where("firstname").is("John")),
Person.class);

13.4.3. 流畅的接口

本节介绍流畅的 API 用法。 请考虑以下简单查询:

Flux<Person> people = template.select(Person.class) 
.all();

使用该方法将表格结果映射到结果对象上。​​Person​​​​select(…)​​​​Person​

读取行返回 a,不限制结果。​​all()​​​​Flux<Person>​

下面的示例声明一个更复杂的查询,该查询按名称、条件和子句指定表名:​​WHERE​​​​ORDER BY​

Mono<Person> first = template.select(Person.class)  
.from("other_person")
.matching(query(where("firstname").is("John")
.and("lastname").in("Doe", "White"))
.sort(by(desc("id"))))
.one();

按名称从表中选择将使用给定的域类型返回行结果。

发出的查询声明条件 onandcolumn 以筛选结果。​​WHERE​​​​firstname​​​​lastname​

结果可以按单个列名排序,从而生成子句。​​ORDER BY​

选择一个结果只会提取一行。 这种使用行的方式期望查询只返回单个结果。如果查询产生多个结果,则发出 a。​​Mono​​​​IncorrectResultSizeDataAccessException​

您可以通过通过以下方式提供目标类型来直接将投影​应用于结果。​​select(Class<?>)​

您可以通过以下终止方法在检索单个实体和检索多个实体之间切换:

  • ​first()​​:仅消耗第一行,返回 a。 如果查询未返回任何结果,则返回的完成而不发出对象。MonoMono
  • ​one()​​:只消耗一行,返回 a。 如果查询未返回任何结果,则返回的完成而不发出对象。 如果查询返回多行,则异常完成发出。MonoMonoMonoIncorrectResultSizeDataAccessException
  • ​all()​​:使用返回 a 的所有返回的行。Flux
  • ​count()​​:应用计数投影返回。Mono<Long>
  • ​exists()​​:返回查询是否通过返回产生任何行。Mono<Boolean>

您可以使用入口点来表达您的查询。 结果查询支持常用的子句 (and) 并支持分页。 流畅的 API 风格使您可以将多个方法链接在一起,同时具有易于理解的代码。 为了提高可读性,您可以使用静态导入来避免使用“new”关键字来创建实例。​​select()​​​​SELECT​​​​SELECT​​​​WHERE​​​​ORDER BY​​​​Criteria​

条件类的方法

Theclass 提供了以下方法,所有这些方法都对应于 SQL 运算符:​​Criteria​

  • ​Criteria​​ :将一个与指定的链式链接添加到当前并返回新创建的链式。(String column)CriteriapropertyCriteria
  • ​Criteria​​ :将链接与指定的链接添加到当前并返回新创建的链。(String column)CriteriapropertyCriteria
  • ​Criteria​​ 大于:使用运算符创建条件。(Object o)>
  • ​Criteria​​ 大于或等于:使用运算符创建条件。(Object o)>=
  • ​Criteria​​ in:通过使用 varargs 参数的运算符创建条件。(Object…​ o)IN
  • ​Criteria​​ in:通过使用集合的运算符创建条件。(Collection<?> collection)IN
  • ​Criteria​​ is:使用列匹配 () 创建条件。(Object o)property = value
  • ​Criteria​​ isNull:使用运算符创建条件。()IS NULL
  • ​Criteria​​ isNotNull:使用运算符创建条件。()IS NOT NULL
  • ​Criteria​​ lessThan:使用运算符创建条件。(Object o)<
  • ​Criteria​​ lessThanOrEquals:使用运算符创建条件。(Object o)
  • ​Criteria​​ like:使用运算符创建条件,而不进行转义字符处理。(Object o)LIKE
  • ​Criteria​​ not:使用运算符创建条件。(Object o)!=
  • ​Criteria​​ notIn:通过使用 varargs 参数的运算符创建条件。(Object…​ o)NOT IN
  • ​Criteria​​ notIn:通过使用集合的运算符创建条件。(Collection<?> collection)NOT IN

您可以使用、和查询。​​Criteria​​​​SELECT​​​​UPDATE​​​​DELETE​

13.4.4. 插入数据

可以使用入口点插入数据。​​insert()​

请考虑以下简单的类型化插入操作:

Mono<Person> insert = template.insert(Person.class) 
.using(new Person("John", "Doe"));

使用方法设置表,基于映射元数据。 它还准备插入语句以接受要插入的对象。​​Person​​​​into(…)​​​​INTO​​​​Person​

提供标量对象。 或者,您可以提供 ato 来运行语句流。 此方法提取所有非值并插入它们。​​Person​​​​Publisher​​​​INSERT​​​​null​

13.4.5. 更新数据

可以使用入口点更新行。 更新数据首先通过接受指定分配来指定要更新的表。 它还接受创建子句。​​update()​​​​Update​​​​Query​​​​WHERE​

请考虑以下简单的类型化更新操作:

Person modified = …

Mono<Long> update = template.update(Person.class)
.inTable("other_table")
.matching(query(where("firstname").is("John")))
.apply(update("age", 42));

更新对象并基于映射元数据应用映射。​​Person​

通过调用方法设置不同的表名。​​inTable(…)​

指定转换为子句的查询。​​WHERE​

应用对象。 在此情况下设置并返回受影响的行数。​​Update​​​​age​​​​42​

13.4.6. 删除数据

可以使用入口点删除行。 删除数据从要从中删除的表的规范开始,并选择性地接受 ato create aclause。​​delete()​​​​Criteria​​​​WHERE​

请考虑以下简单的插入操作:

Mono<Long> delete = template.delete(Person.class) 
.from("other_table")
.matching(query(where("firstname").is("John")))
.all();

删除对象并基于映射元数据应用映射。​​Person​

通过调用方法设置不同的表名。​​from(…)​

指定转换为子句的查询。​​WHERE​

应用删除操作并返回受影响的行数。

14. R2DBC 存储库

本章指出了 R2DBC 存储库支持的特点。 本章建立在使用 Spring 数据存储库中解释的核心存储库支持之上。 在阅读本章之前,您应该对其中解释的基本概念有很好的理解。

14.1. 用法

要访问存储在关系数据库中的域实体,您可以使用我们复杂的存储库支持,这大大简化了实施。 为此,请为存储库创建一个接口。 请考虑以下类:​​Person​

例 51。示例人员实体

public class Person {

@Id
private Long id;
private String firstname;
private String lastname;

// … getters and setters omitted
}

以下示例显示了前面类的存储库接口:​​Person​

例 52。用于保留人员实体的基本存储库接口

public interface PersonRepository extends ReactiveCrudRepository<Person, Long> {

// additional custom query methods go here
}

要配置 R2DBC 存储库,可以使用注释。 如果未配置基本包,基础结构将扫描带批注的配置类的包。 以下示例显示如何将 Java 配置用于存储库:​​@EnableR2dbcRepositories​

例 53。存储库的 Java 配置

@Configuration
@EnableR2dbcRepositories
class ApplicationConfig extends AbstractR2dbcConfiguration {

@Override
public ConnectionFactory connectionFactory() {
return …
}
}

由于我们的域存储库可扩展,因此它为您提供了响应式 CRUD 操作来访问实体。 除此之外,还有,它添加了类似于的附加排序功能。 使用存储库实例只是将其注入客户端的依赖项问题。 因此,您可以使用以下代码检索所有对象:​​ReactiveCrudRepository​​​​ReactiveCrudRepository​​​​ReactiveSortingRepository​​​​PagingAndSortingRepository​​​​Person​

例 54。对个人实体的分页访问

@ExtendWith(SpringExtension.class)
@ContextConfiguration
class PersonRepositoryTests {

@Autowired
PersonRepository repository;

@Test
void readsAllEntitiesCorrectly() {

repository.findAll()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}

@Test
void readsEntitiesByNameCorrectly() {

repository.findByFirstname("Hello World")
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}

前面的示例使用Spring的单元测试支持创建了一个应用程序上下文,该上下文将基于注释的依赖项注入到测试用例中。 在测试方法中,我们使用存储库来查询数据库。 我们用作测试辅助工具,以验证我们对结果的期望。​​StepVerifier​

14.2. 查询方法

通常在存储库上触发的大多数数据访问操作都会导致对数据库运行查询。 定义此类查询是在存储库接口上声明方法的问题,如以下示例所示:

例 55。具有查询方法的人员存储库

interface ReactivePersonRepository extends ReactiveSortingRepository<Person, Long> {

Flux<Person> findByFirstname(String firstname);

Flux<Person> findByFirstname(Publisher<String> firstname);

Flux<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable);

Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);

Mono<Person> findFirstByLastname(String lastname);

@Query("SELECT * FROM person WHERE lastname = :lastname")
Flux<Person> findByLastname(String lastname);

@Query("SELECT firstname, lastname FROM person WHERE lastname = $1")
Mono<Person> findFirstByLastname(String lastname);
}

该方法显示所有给定人员的查询。查询是通过分析可与 and 连接的约束的方法名称派生的。因此,方法名称导致查询表达式。​​firstname​​​​And​​​​Or​​​​SELECT … FROM person WHERE firstname = :firstname​

该方法显示所有具有给定一次的查询的人是由给定的发出的。​​firstname​​​​firstname​​​​Publisher​

用于将偏移量和排序参数传递给数据库。​​Pageable​

查找给定条件的单个实体。它以非唯一结果完成。​​IncorrectResultSizeDataAccessException​

除非 <4>,否则即使查询生成更多结果行,也始终发出第一个实体。

该方法显示具有给定姓氏的所有用户的查询。​​findByLastname​

对仅投影和列的单个实体的查询。 带批注的查询使用本机绑定标记,在此示例中是 Postgres 绑定标记。​​Person​​​​firstname​​​​lastname​

请注意,注释中使用的 select 语句的列必须与相应属性生成的名称匹配。 如果 select 语句不包含匹配的列,则不设置该属性。如果持久性构造函数需要该属性,则提供 null 或(对于基元类型)默认值。​​@Query​​​​NamingStrategy​

下表显示了查询方法支持的关键字:

表 2.查询方法支持的关键字

关键词

样本

逻辑结果

​After​

​findByBirthdateAfter(Date date)​

​birthdate > date​

​GreaterThan​

​findByAgeGreaterThan(int age)​

​age > age​

​GreaterThanEqual​

​findByAgeGreaterThanEqual(int age)​

​age >= age​

​Before​

​findByBirthdateBefore(Date date)​

​birthdate < date​

​LessThan​

​findByAgeLessThan(int age)​

​age < age​

​LessThanEqual​

​findByAgeLessThanEqual(int age)​

​age <= age​

​Between​

​findByAgeBetween(int from, int to)​

​age BETWEEN from AND to​

​NotBetween​

​findByAgeNotBetween(int from, int to)​

​age NOT BETWEEN from AND to​

​In​

​findByAgeIn(Collection<Integer> ages)​

​age IN (age1, age2, ageN)​

​NotIn​

​findByAgeNotIn(Collection ages)​

​age NOT IN (age1, age2, ageN)​

​IsNotNull​​​, ​​NotNull​

​findByFirstnameNotNull()​

​firstname IS NOT NULL​

​IsNull​​​, ​​Null​

​findByFirstnameNull()​

​firstname IS NULL​

​Like​​​, , ​​StartingWith​​​​EndingWith​

​findByFirstnameLike(String name)​

​firstname LIKE name​

​NotLike​​​, ​​IsNotLike​

​findByFirstnameNotLike(String name)​

​firstname NOT LIKE name​

​Containing​​在字符串上

​findByFirstnameContaining(String name)​

​firstname LIKE '%' + name +'%'​

​NotContaining​​在字符串上

​findByFirstnameNotContaining(String name)​

​firstname NOT LIKE '%' + name +'%'​

​(No keyword)​

​findByFirstname(String name)​

​firstname = name​

​Not​

​findByFirstnameNot(String name)​

​firstname != name​

​IsTrue​​​, ​​True​

​findByActiveIsTrue()​

​active IS TRUE​

​IsFalse​​​, ​​False​

​findByActiveIsFalse()​

​active IS FALSE​

14.2.1. 修改查询

前面的部分介绍如何声明查询以访问给定实体或实体集合。 使用上表中的关键字可以与创建删除匹配行的派生查询结合使用。​​delete…By​​​​remove…By​

例 56.查询​​Delete…By​

interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {

Mono<Integer> deleteByLastname(String lastname);

Mono<Void> deletePersonByLastname(String lastname);

Mono<Boolean> deletePersonByLastname(String lastname);
}

使用 return 类型 of 返回受影响的行数。​​Mono<Integer>​

使用仅报告是否成功删除行而不发出结果值。​​Void​

使用报告是否至少删除了一行。​​Boolean​

由于此方法对于全面的自定义功能是可行的,因此可以通过批注查询方法来修改只需要参数绑定的查询,如以下示例所示:​​@Modifying​

@Modifying
@Query("UPDATE person SET firstname = :firstname where lastname = :lastname")
Mono<Integer> setFixedFirstnameFor(String firstname, String lastname);

修改查询的结果可以是:

  • ​Void​​(或 Kotlin)丢弃更新计数并等待完成。Unit
  • ​Integer​​或发出受影响行计数的其他数值类型。
  • ​Boolean​​以发出是否至少更新了一行。

注释仅与注释结合使用时才相关。 派生的自定义方法不需要此批注。​​@Modifying​​​​@Query​

修改查询直接针对数据库执行。 不会调用任何事件或回调。 因此,如果带有审核批注的字段未在批注查询中更新,则不会更新这些批注。

或者,您可以使用Spring 数据存储库的自定义实现中描述的工具添加自定义修改行为。

14.2.2. 使用 SpEL 表达式进行查询

查询字符串定义可以与 SpEL 表达式一起使用,以便在运行时创建动态查询。 SpEL 表达式可以提供在运行查询之前立即计算的谓词值。

表达式通过包含所有参数的数组公开方法参数。 以下查询用于声明谓词值(等效于参数绑定):​​[0]​​​​lastname​​​​:lastname​

@Query("SELECT * FROM person WHERE lastname = :#{[0]}")
Flux<Person> findByQueryWithExpression(String lastname);

查询字符串中的 SpEL 是增强查询的强大方法。 但是,他们也可以接受各种不需要的论点。 在将字符串传递给查询之前,应确保对其进行清理,以避免对查询进行不必要的更改。

表达式支持可通过查询 SPI: 进行扩展。 查询 SPI 可以提供属性和函数,并且可以自定义根对象。 在生成查询时,在进行 SpEL 评估时,将从应用程序上下文中检索扩展。​​org.springframework.data.spel.spi.EvaluationContextExtension​

将 SpEL 表达式与纯参数结合使用时,请使用命名参数表示法而不是本机绑定标记,以确保正确的绑定顺序。

14.2.3. 按示例查询

Spring Data R2DBC 还允许您使用按示例查询来设置查询方式。 此技术允许您使用“探测器”对象。 本质上,任何不为空或将用于匹配的字段。​​null​

下面是一个示例:

Employee employee = new Employee(); 
employee.setName("Frodo");

Example<Employee> example = Example.of(employee);

Flux<Employee> employees = repository.findAll(example);

// do whatever with the flux

使用条件创建域对象(字段将被忽略)。​​null​

使用域对象,创建一个.​​Example​

通过,执行查询(用于a)。​​R2dbcRepository​​​​findOne​​​​Mono​

这说明了如何使用域对象创建简单的探测。 在这种情况下,它将基于对象的字段等于进行查询。​​Employee​​​​name​​​​Frodo​​​​null​

Employee employee = new Employee();
employee.setName("Baggins");
employee.setRole("ring bearer");

ExampleMatcher matcher = matching()
.withMatcher("name", endsWith())
.withIncludeNullValues()
.withIgnorePaths("role");
Example<Employee> example = Example.of(employee, matcher);

Flux<Employee> employees = repository.findAll(example);

// do whatever with the flux

创建在所有字段上匹配的自定义项(用于匹配任何字段)​​ExampleMatcher​​​​matchingAny()​

对于字段,请使用与字段末尾匹配的通配符​​name​

匹配列(不要忘记这不等于关系数据库)。​​null​​​​NULL​​​​NULL​

形成查询时忽略该字段。​​role​

将自定义插入探头。​​ExampleMatcher​

还可以对任何属性应用 a,从而允许您在形成查询之前转换属性。 例如,可以在创建查询之前应用 ato 基于 a 的属性。​​withTransform()​​​​toUpperCase()​​​​String​

当您事先不知道查询中所需的所有字段时,按示例查询确实大放异彩。 如果要在用户可以选择字段的网页上构建筛选器,则“按示例查询”是灵活地将其捕获到高效查询中的好方法。

Spring Data(数据)R2DBC

14.2.4. 实体状态检测策略

下表描述了 Spring Data 提供的用于检测实体是否为新实体的策略:

表 3.用于检测实体是否是 Spring 数据中的新实体的选项

​@Id​​-属性检查(默认)

默认情况下,Spring 数据检查给定实体的标识符属性。 如果标识符属性 isorin 在基元类型的情况下,则假定该实体是新的。 否则,假定它不是新的。​​null​​​​0​

​@Version​​-物业检查

如果存在带批注的属性,或者如果是基元类型的版本属性,则认为该实体是新的。 如果 version 属性存在但具有不同的值,则该实体被视为不是新的。 如果不存在版本属性,Spring 数据将回退到标识符属性的检查。​​@Version​​​​null​​​​0​

实施​​Persistable​

如果一个实体实现了,Spring 数据会将新的检测委托给实体的方法。 有关详细信息,请参阅Javadoc​。​​Persistable​​​​isNew(…)​

注意:如果使用​​AccessType.PROPERTY,​​​则会检测并持久化​​Persistable​​​的属性。 为避免这种情况,请使用​​@Transient​​。

提供自定义实现​​EntityInformation​

您可以通过创建特定于模块的存储库工厂的子类并重写该方法来自定义存储库基本实现中使用的抽象。 然后,您必须将特定于模块的存储库工厂的自定义实现注册为 Spring Bean。 请注意,这应该很少是必需的。​​EntityInformation​​​​getEntityInformation(…)​

14.2.5. ID 生成

Spring Data R2DBC使用ID来识别实体。 实体的ID必须用Spring Data的注释进行注释​​@Id​​。

当数据库具有 ID 列的自动增量列时,生成的值在将其插入数据库后在实体中设置。

Spring Data R2DBC 不会尝试在实体是新实体且标识符值默认为其初始值时插入标识符列的值。 也就是说,对于基元类型,并且标识符属性使用数字包装器类型,例如。​​0​​​​null​​​​Long​

一个重要的约束是,在保存实体后,该实体不得再是新的。 请注意,实体是否为新实体是实体状态的一部分。 对于自动增量列,这会自动发生,因为 Spring Data 使用 ID 列中的值设置了 ID。

14.2.6. 乐观锁定

注释提供类似于 R2DBC 上下文中的 JPA 语法,并确保更新仅应用于具有匹配版本的行。 因此,版本属性的实际值将添加到更新查询中,这样,如果另一个操作同时更改了该行,则更新不会产生任何影响。 在这种情况下,茴香扔了。 以下示例显示了这些功能:​​@Version​​​​OptimisticLockingFailureException​

@Table
class Person {

@Id Long id;
String firstname;
String lastname;
@Version Long version;
}

R2dbcEntityTemplate template = …;

Mono<Person> daenerys = template.insert(new Person("Daenerys"));

Person other = template.select(Person.class)
.matching(query(where("id").is(daenerys.getId())))
.first().block();

daenerys.setLastname("Targaryen");
template.update(daenerys);

template.update(other).subscribe(); // emits OptimisticLockingFailureException

最初插入 row.is 设置为。​​version​​​​0​

加载刚刚插入的 row.is。​​version​​​​0​

更新行。设置和碰撞。​​version = 0​​​​lastname​​​​version​​​​1​

尝试更新以前加载的行,该行仍然存在。操作失败,并显示 ,因为电流是。​​version = 0​​​​OptimisticLockingFailureException​​​​version​​​​1​

14.2.7. 预测

Spring 数据查询方法通常返回由存储库管理的聚合根的一个或多个实例。 但是,有时可能需要基于这些类型的某些属性创建投影。 Spring 数据允许对专用返回类型进行建模,以更有选择性地检索托管聚合的部分视图。

假设存储库和聚合根类型,如以下示例所示:

例 57。示例聚合和存储库

class Person {

@Id UUID id;
String firstname, lastname;
Address address;

static class Address {
String zipCode, city, street;
}
}

interface PersonRepository extends Repository<Person, UUID> {

Flux<Person> findByLastname(String lastname);
}

现在假设我们只想检索人员的姓名属性。 Spring Data提供了什么手段来实现这一目标?本章的其余部分将回答这个问题。

基于接口的投影

将查询结果限制为仅 name 属性的最简单方法是声明一个接口,该接口公开要读取的属性的访问器方法,如以下示例所示:

例 58。用于检索属性子集的投影接口

interface NamesOnly {

String getFirstname();
String getLastname();
}

这里重要的一点是,此处定义的属性与聚合根中的属性完全匹配。 这样做可以添加查询方法,如下所示:

例 59。使用基于接口的投影和查询方法的存储库

interface PersonRepository extends Repository<Person, UUID> {

Flux<NamesOnly> findByLastname(String lastname);
}

查询执行引擎在运行时为返回的每个元素创建该接口的代理实例,并将对公开方法的调用转发到目标对象。

在 yourThat 中声明方法会覆盖基方法(例如,声明在、特定于存储的存储库接口或 the)会导致对基方法的调用,而不管声明的返回类型如何。请确保使用兼容的返回类型,因为基方法不能用于投影。某些存储模块支持注释,以将重写的基方法转换为查询方法,然后可用于返回投影。​​Repository​​​​CrudRepository​​​​Simple…Repository​​​​@Query​

投影可以递归使用。如果还希望包含某些信息,请为其创建一个投影接口,并从声明中返回该接口,如以下示例所示:​​Address​​​​getAddress()​

例 60。用于检索属性子集的投影接口

interface PersonSummary {

String getFirstname();
String getLastname();
AddressSummary getAddress();

interface AddressSummary {
String getCity();
}
}

在方法调用时,将获取目标实例的属性并依次包装到投影代理中。​​address​

封闭式投影

其访问器方法都与目标聚合的属性匹配的投影接口被视为封闭投影。以下示例(我们在本章前面也使用过)是一个封闭投影:

例 61。封闭投影

interface NamesOnly {

String getFirstname();
String getLastname();
}

如果您使用封闭投影,Spring Data 可以优化查询执行,因为我们知道支持投影代理所需的所有属性。 有关这方面的更多详细信息,请参阅参考文档中特定于模块的部分。

开放投影

投影接口中的访问器方法还可用于通过注释来计算新值,如以下示例所示:​​@Value​

例 62。开放式投影

interface NamesOnly {

@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();

}

支持投影的聚合根在变量中可用。 投影接口使用是开放式投影。 在这种情况下,Spring 数据无法应用查询执行优化,因为 SpEL 表达式可以使用聚合根的任何属性。​​target​​​​@Value​

使用的表达式不应该太复杂 — 您希望避免对变量进行编程。 对于非常简单的表达式,一种选择可能是采用默认方法(Java 8 中引入),如以下示例所示:​​@Value​​​​String​

例 63。使用自定义逻辑的默认方法的投影接口

interface NamesOnly {

String getFirstname();
String getLastname();

default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}

此方法要求您能够完全基于投影接口上公开的其他访问器方法实现逻辑。 第二个更灵活的选项是在 Spring Bean 中实现自定义逻辑,然后从 SpEL 表达式中调用该逻辑,如以下示例所示:

例 64。示例人员对象

@Component
class MyBean {

String getFullName(Person person) {

}
}

interface NamesOnly {

@Value("#{@myBean.getFullName(target)}")
String getFullName();

}

请注意 SpEL 表达式如何引用和调用该方法,并将投影目标作为方法参数转发。 SpEL 表达式评估支持的方法也可以使用方法参数,然后可以从表达式中引用这些参数。 方法参数可通过名为的数组获得。下面的示例演示如何从数组中获取方法参数:​​myBean​​​​getFullName(…)​​​​Object​​​​args​​​​args​

例 65。示例人员对象

interface NamesOnly {

@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}

同样,对于更复杂的表达式,您应该使用 Spring Bean 并让表达式调用方法,如前所述。

可为空的包装器

投影接口中的 Getter 可以使用可为空的包装器来提高空安全性。目前支持的包装器类型包括:

  • ​java.util.Optional​
  • ​com.google.common.base.Optional​
  • ​scala.Option​
  • ​io.vavr.control.Option​

例 66。使用可为空包装器的投影接口

interface NamesOnly {

Optional<String> getFirstname();
}

如果基础投影值不是,则使用包装器的当前表示形式返回值。 如果支持值为 ,则 getter 方法返回所用包装类型的空表示形式。​​null​​​​null​

基于类的投影 (DTO)

定义投影的另一种方法是使用值类型 DTO(数据传输对象),用于保存应检索的字段的属性。 这些 DTO 类型的使用方式与投影接口的使用方式完全相同,只是不会发生代理,并且不能应用嵌套投影。

如果存储通过限制要加载的字段来优化查询执行,则要加载的字段由公开的构造函数的参数名称确定。

以下示例显示了一个投影 DTO:

例 67。一个突出的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 的样板代码


你可以通过使用ProjectLombok​来大大简化DTO的代码,它提供了anannotation(不要与前面的接口示例中所示的Spring'sannotation混淆)。 如果您使用龙目岛项目的注释,前面显示的示例 DTO 将变为以下内容:​​@Value​​​​@Value​​​​@Value​




@Value
class NamesOnly {
String firstname, lastname;
}




字段是默认的,该类公开一个构造函数,该构造函数采用所有字段并自动获取实现的 sand方法。​​private final​​​​equals(…)​​​​hashCode()​


动态投影

到目前为止,我们已经使用投影类型作为集合的返回类型或元素类型。 但是,您可能希望选择要在调用时使用的类型(这使其成为动态的)。 若要应用动态投影,请使用查询方法,如以下示例所示:

例 68。使用动态投影参数的存储库

interface PersonRepository extends Repository<Person, UUID> {

<T> Flux<T> findByLastname(String lastname, Class<T> type);
}

这样,该方法可用于按原样获取聚合或应用投影,如以下示例所示:

例 69。使用具有动态投影的存储库

void someMethod(PersonRepository people) {

Flux<Person> aggregates =
people.findByLastname("Matthews", Person.class);

Flux<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}

检查类型的查询参数是否符合动态投影参数的条件。 如果查询的实际返回类型等于参数的泛型参数类型,则匹配参数不可用于查询或 SpEL 表达式。 如果要使用 aparameter 作为查询参数,请确保使用其他泛型参数,例如。​​Class​​​​Class​​​​Class​​​​Class​​​​Class<?>​

结果映射

返回接口或 DTO 投影的查询方法由实际查询生成的结果提供支持。 接口投影通常依赖于首先将结果映射到域类型以考虑潜在类型映射,而实际投影代理使用可能部分具体化的实体来公开投影数据。​​@Column​

DTO 投影的结果映射取决于实际查询类型。 派生查询使用域类型来映射结果,Spring Data 仅从域类型上可用的属性创建 DTO 实例。 不支持在 DTO 中声明域类型上不可用的属性。

基于字符串的查询使用不同的方法,因为实际查询(特别是字段投影)和结果类型声明非常接近。 与用查询方法注释的 DTO 投影将查询结果直接映射到 DTO 类型中。 不考虑域类型的字段映射。 直接使用 DTO 类型,查询方法可以从不限于域模型的更动态的投影中受益。​​@Query​

14.3. 实体回调

Spring 数据基础架构提供了用于在调用某些方法之前和之后修改实体的钩子。 这些所谓的实例提供了一种方便的方法,可以检查并可能以回调样式修改实体。
安看起来很像一个专业的人。 一些 Spring 数据模块发布存储允许修改给定实体的特定事件(例如)。在某些情况下,例如在使用不可变类型时,这些事件可能会导致麻烦。 此外,事件发布依赖于。如果使用异步配置它可能会导致不可预测的结果,因为事件处理可以分叉到线程上。​​EntityCallback​​​​EntityCallback​​​​ApplicationListener​​​​BeforeSaveEvent​​​​ApplicationEventMulticaster​​​​TaskExecutor​

实体回调为集成点提供同步和反应式 API,以保证在处理链中定义良好的检查点按顺序执行,返回可能修改的实体或反应式包装器类型。

实体回调通常按 API 类型分隔。这种分离意味着同步 API 仅考虑同步实体回调,而反应式实现仅考虑反应式实体回调。


实体回调 API 已在 Spring Data Commons 2.2 中引入。这是应用实体修改的推荐方法。 现有存储特定仍会在调用可能注册的实例之前发布。​​ApplicationEvents​​​​EntityCallback​


14.3.1. 实现实体回调

Anis 通过其泛型类型参数与其域类型直接关联。 每个 Spring 数据模块通常附带一组涵盖实体生命周期的预定义接口。​​EntityCallback​​​​EntityCallback​

例 70。剖析​​EntityCallback​

@FunctionalInterface
public interface BeforeSaveCallback<T> extends EntityCallback<T> {

/**
* Entity callback method invoked before a domain object is saved.
* Can return either the same or a modified instance.
*
* @return the domain object to be persisted.
*/
T onBeforeSave(T entity <2>, String collection <3>);
}

​BeforeSaveCallback​​在保存实体之前要调用的特定方法。返回可能已修改的实例。

持久之前的实体。

许多存储特定的参数,例如实体保存到的集合

例 71。反应性解剖​​EntityCallback​

@FunctionalInterface
public interface ReactiveBeforeSaveCallback<T> extends EntityCallback<T> {

/**
* Entity callback method invoked on subscription, before a domain object is saved.
* The returned Publisher can emit either the same or a modified instance.
*
* @return Publisher emitting the domain object to be persisted.
*/
Publisher<T> onBeforeSave(T entity <2>, String collection <3>);
}

​BeforeSaveCallback​​在保存实体之前,要在订阅上调用的特定方法。发出可能经过修改的实例。

持久之前的实体。

许多存储特定的参数,例如实体保存到的集合

可选的实体回调参数由实现 Spring 数据模块定义,并从调用站点推断。​​EntityCallback.callback()​

实现适合您的应用程序需求的接口,如以下示例所示:

例 72。例​​BeforeSaveCallback​

class DefaultingEntityCallback implements BeforeSaveCallback<Person>, Ordered {      

@Override
public Object onBeforeSave(Person entity, String collection) {

if(collection == "user") {
return // ...
}

return // ...
}

@Override
public int getOrder() {
return 100;
}
}

根据您的需求实现回调。

如果存在同一域类型的多个实体回调,则可能会对实体回调进行排序。排序遵循最低优先级。

14.3.2. 注册实体回调

​EntityCallback​​如果 bean 在 中注册,则由商店特定的实现拾取。 大多数模板 API 已经实现,因此可以访问​​ApplicationContext​​​​ApplicationContextAware​​​​ApplicationContext​

以下示例说明了有效实体回调注册的集合:

例 73。示例 Bean 注册​​EntityCallback​

@Order(1)                                                           
@Component
class First implements BeforeSaveCallback<Person> {

@Override
public Person onBeforeSave(Person person) {
return // ...
}
}

@Component
class DefaultingEntityCallback implements BeforeSaveCallback<Person>,
Ordered {

@Override
public Object onBeforeSave(Person entity, String collection) {
// ...
}

@Override
public int getOrder() {
return 100;
}
}

@Configuration
public class EntityCallbackConfiguration {

@Bean
BeforeSaveCallback<Person> unorderedLambdaReceiverCallback() {
return (BeforeSaveCallback<Person>) it -> // ...
}
}

@Component
class UserCallbacks implements BeforeConvertCallback<User>,
BeforeSaveCallback<User> {

@Override
public Person onBeforeConvert(User user) {
return // ...
}

@Override
public Person onBeforeSave(User user) {
return // ...
}
}

​BeforeSaveCallback​​​从注释中接收其命令。​​@Order​

​BeforeSaveCallback​​​通过接口实现接收其订单。​​Ordered​

​BeforeSaveCallback​​​使用 lambda 表达式。默认无序,最后调用。请注意,由 lambda 表达式实现的回调不会公开类型信息,因此使用不可分配的实体调用这些信息会影响回调吞吐量。使用 aorto 为回调 Bean 启用类型筛选。​​class​​​​enum​

在单个实现类中组合多个实体回调接口。

14.3.3. 存储特定的实体回调

Spring Data R2DBC使用API作为其审计支持,并对以下回调做出反应。​​EntityCallback​

表 4.支持的实体回调

回调

方法

描述

次序

之前转换回调

​onBeforeConvert(T entity, SqlIdentifier table)​

在将域对象转换为之前调用。​​OutboundRow​

​Ordered.LOWEST_PRECEDENCE​

转换后回调

​onAfterConvert(T entity, SqlIdentifier table)​

加载域对象后调用。
可以从行中读取域对象后对其进行修改。

​Ordered.LOWEST_PRECEDENCE​

审计实体回调

​onBeforeConvert(T entity, SqlIdentifier table)​

标记已创建修改的可审核实体

100

之前保存回调

​onBeforeSave(T entity, OutboundRow row, SqlIdentifier table)​

在保存域对象之前调用。
可以修改目标,要持久化,包含所有映射的实体信息。​​​OutboundRow​

​Ordered.LOWEST_PRECEDENCE​

保存后回调

​onAfterSave(T entity, OutboundRow row, SqlIdentifier table)​

保存域对象后调用。
可以修改域对象,保存后返回,包含所有映射的实体信息。​​​OutboundRow​

​Ordered.LOWEST_PRECEDENCE​

14.4. 使用多个数据库

使用多个可能不同的数据库时,应用程序将需要不同的配置方法。 提供的支持类假定一个从中派生的单个类。 话虽如此,您需要自己定义一些 bean 来配置 Spring Data R2DBC 以使用多个数据库。​​AbstractR2dbcConfiguration​​​​ConnectionFactory​​​​Dialect​

R2DBC 存储库需要实现存储库。 无需使用即可扫描存储库的简单配置如下所示:​​R2dbcEntityOperations​​​​AbstractR2dbcConfiguration​

@Configuration
@EnableR2dbcRepositories(basePackages = "com.acme.mysql", entityOperationsRef = "mysqlR2dbcEntityOperations")
static class MySQLConfiguration {

@Bean
@Qualifier("mysql")
public ConnectionFactory mysqlConnectionFactory() {
return …
}

@Bean
public R2dbcEntityOperations mysqlR2dbcEntityOperations(@Qualifier("mysql") ConnectionFactory connectionFactory) {

DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);

return new R2dbcEntityTemplate(databaseClient, MySqlDialect.INSTANCE);
}
}

请注意,允许通过 or 进行配置。 在连接到同一类型的多个数据库时,使用 variousbean 很有用。 当使用方言不同的不同数据库系统时,请使用(entityOperationsRef = ...)'相反。​​@EnableR2dbcRepositories​​​​databaseClientRef​​​​entityOperationsRef​​​​DatabaseClient​​​​@EnableR2dbcRepositories​

15. 审计

15.1. 基础知识

Spring Data提供了复杂的支持,可以透明地跟踪谁创建或更改了实体以及更改发生的时间。若要从该功能中受益,必须为实体类配备可以使用批注或通过实现接口来定义的审核元数据。 此外,必须通过注释配置或 XML 配置启用审核,以注册所需的基础结构组件。 有关配置示例,请参阅特定于商店的部分。


不需要仅跟踪创建和修改日期的应用程序会使其实体实现AuditorAware。


15.1.1. 基于注释的审计元数据

我们提供捕获创建或修改实体的用户以及何时发生更改的捕获。​​@CreatedBy​​​​@LastModifiedBy​​​​@CreatedDate​​​​@LastModifiedDate​

例 74。被审计的实体

class Customer {

@CreatedBy
private User user;

@CreatedDate
private Instant createdDate;

// … further properties omitted
}

如您所见,注释可以有选择地应用,具体取决于要捕获的信息。 指示在进行更改时捕获的注释可用于 JDK8 日期和时间类型,,,以及旧版 Javaand 的属性。​​long​​​​Long​​​​Date​​​​Calendar​

审核元数据不一定需要位于根级实体中,但可以添加到嵌入的实体中(取决于使用的实际存储),如下面的代码片段所示。

例 75。审核嵌入实体中的元数据

class Customer {

private AuditMetadata auditingMetadata;

// … further properties omitted
}

class AuditMetadata {

@CreatedBy
private User user;

@CreatedDate
private Instant createdDate;

}

15.1.2. 基于接口的审计元数据

如果您不想使用注释来定义审核元数据,则可以让您的域类实现接口。它公开所有审核属性的 setter 方法。​​Auditable​

15.1.3. ​​AuditorAware​

如果使用任一 or,则审计基础结构需要以某种方式了解当前主体。为此,我们提供了 anSPI 接口,您必须实现该接口来告诉基础架构当前与应用程序交互的用户或系统是谁。泛型类型定义批注属性的类型。​​@CreatedBy​​​​@LastModifiedBy​​​​AuditorAware<T>​​​​T​​​​@CreatedBy​​​​@LastModifiedBy​

以下示例显示了使用 Spring 安全性对象的接口的实现:​​Authentication​

例 76。基于弹簧安全性的实现​​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 安全性提供的对象,并查找您在实现中创建的自定义实例。我们在这里假设您通过实现公开域用户,但基于发现,您也可以从任何地方查找它。​​Authentication​​​​UserDetails​​​​UserDetailsService​​​​UserDetails​​​​Authentication​

15.1.4. ​​ReactiveAuditorAware​

使用响应式基础结构时,您可能希望利用上下文信息来提供信息。 我们提供 anSPI 接口,您必须实现该接口来告诉基础架构当前与应用程序交互的用户或系统是谁。泛型类型定义批注属性的类型。​​@CreatedBy​​​​@LastModifiedBy​​​​ReactiveAuditorAware<T>​​​​T​​​​@CreatedBy​​​​@LastModifiedBy​

以下示例显示了使用反应式 Spring 安全性对象的接口的实现:​​Authentication​

例 77。基于弹簧安全性的实现​​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 安全性提供的对象,并查找您在实现中创建的自定义实例。我们在这里假设您通过实现公开域用户,但基于发现,您也可以从任何地方查找它。​​Authentication​​​​UserDetails​​​​UserDetailsService​​​​UserDetails​​​​Authentication​

15.2. R2DBC 的一般审计配置

从 Spring Data R2DBC 1.2 开始,可以通过使用注释注释配置类来启用审计,如以下示例所示:​​@EnableR2dbcAuditing​

例 78。使用 JavaConfig 激活审计

@Configuration
@EnableR2dbcAuditing
class Config {

@Bean
public ReactiveAuditorAware<AuditableUser> myAuditorProvider() {
return new AuditorAwareImpl();
}
}

如果公开 typeto 的 bean,则审核基础结构会自动选取它,并使用它来确定要在域类型上设置的当前用户。 如果在中注册了多个实现,则可以通过显式设置属性来选择要使用的一个。​​ReactiveAuditorAware​​​​ApplicationContext​​​​ApplicationContext​​​​auditorAwareRef​​​​@EnableR2dbcAuditing​

16. 映射

丰富的映射支持由 .have 提供,该模型允许将域对象映射到数据行。 映射元数据模型通过使用域对象上的注释进行填充。 但是,基础结构不仅限于使用注释作为元数据信息的唯一来源。 它还允许您通过遵循一组约定将对象映射到行,而无需提供任何其他元数据。​​MappingR2dbcConverter​​​​MappingR2dbcConverter​​​​MappingR2dbcConverter​

本节介绍的功能,包括如何使用约定将对象映射到行,以及如何使用基于注释的映射元数据覆盖这些约定。​​MappingR2dbcConverter​

16.1. 对象映射基础

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

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

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

16.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 数据将通过反射回退到实体实例化。

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

例 79。生成的属性访问器

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 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的属性访问器。

让我们看一下以下实体:

例 80。示例实体

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​

16.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)

16.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)

16.2. 基于约定的映射

​MappingR2dbcConverter​​具有一些约定,用于在未提供其他映射元数据时将对象映射到行。 这些约定是:

  • 短 Java 类名按以下方式映射到表名。 类映射到表名。 相同的名称映射应用于将字段映射到列名称。 例如,字段映射到列。 您可以通过提供自定义来控制此映射。有关更多详细信息,请参阅映射配置。 默认情况下,从属性或类名派生的表名和列名在 SQL 语句中使用,不带引号。 您可以通过设置来控制此行为。com.bigbank.SavingsAccountSAVINGS_ACCOUNTfirstNameFIRST_NAMENamingStrategyR2dbcMappingContext.setForceQuote(true)
  • 不支持嵌套对象。
  • 转换器使用向其注册的任何 Spring 转换器来覆盖对象属性到行列和值的默认映射。
  • 对象的字段用于与行中的列相互转换。 不使用公共属性。JavaBean
  • 如果有一个非零参数构造函数,其构造函数参数名称与行的*列名称匹配,则使用该构造函数。 否则,将使用零参数构造函数。 如果有多个非零参数构造函数,则会引发异常。

16.3. 映射配置

默认情况下(除非显式配置)创建 的实例。 您可以创建自己的实例。 通过创建自己的实例,您可以注册 Spring 转换器以将特定类映射到数据库和从数据库映射特定类。​​MappingR2dbcConverter​​​​DatabaseClient​​​​MappingR2dbcConverter​

您可以使用基于 Java 的元数据来配置它们。以下示例使用 Spring 基于 Java 的配置:​​MappingR2dbcConverter​​​​DatabaseClient​​​​ConnectionFactory​

如果设置了 true,则从类和属性派生的表名和列名将与特定于数据库的引号一起使用。 这意味着可以在这些名称中使用保留的 SQL 字(如顺序)。 您可以通过覆盖来执行此操作。 Spring Data 将此类名称的字母大小写转换为未使用引号时配置的数据库也使用的形式。 因此,在创建表时可以使用不带引号的名称,只要在名称中不使用关键字或特殊字符即可。 对于遵循 SQL 标准的数据库,这意味着名称将转换为大写。 引号字符和名称大写的方式由使用的名称控制。 请参阅R2DBC 驱动程序了解如何配置自定义方言。​​setForceQuote​​​​R2dbcMappingContext to​​​​r2dbcMappingContext(Optional<NamingStrategy>)​​​​AbstractR2dbcConfiguration​​​​Dialect​

例 81。@Configuration类来配置 R2DBC 映射支持

@Configuration
public class MyAppConfig extends AbstractR2dbcConfiguration {

public ConnectionFactory connectionFactory() {
return ConnectionFactories.get("r2dbc:…");
}

// the following are optional

@Override
protected List<Object> getCustomConverters() {
return List.of(new PersonReadConverter(), new PersonWriteConverter());
}
}

​AbstractR2dbcConfiguration​​要求您实现定义 a 的方法。​​ConnectionFactory​

您可以通过重写方法向转换器添加其他转换器。​​r2dbcCustomConversions​

您可以通过将自定义注册为 Bean 来配置自定义。 控制如何将类和属性的名称转换为表和列的名称。​​NamingStrategy​​​​NamingStrategy​

​AbstractR2dbcConfiguration​​​创建一个实例,并将其注册到 OF 名称下的容器。​​DatabaseClient​​​​databaseClient​

16.4. 基于元数据的映射

为了充分利用 Spring Data R2DBC 支持中的对象映射功能,您应该使用注释来注释映射的对象。 尽管映射框架不必具有此注释(即使没有任何注释,您的 POJO 也已正确映射),但它允许类路径扫描程序查找并预处理域对象以提取必要的元数据。 如果不使用此注释,则应用程序在首次存储域对象时性能会略有下降,因为映射框架需要构建其内部元数据模型,以便它了解域对象的属性以及如何持久化它们。 以下示例显示了一个域对象:​​@Table​

例 82。示例域对象

package com.mycompany.domain;

@Table
public class Person {

@Id
private Long id;

private Integer ssn;

private String firstName;

private String lastName;
}

注释告诉映射器要用作主键的属性。​​@Id​

16.4.1. 默认类型映射

下表说明了实体的属性类型如何影响映射:

源类型

目标类型

言论

基元类型和包装器

直通

可以使用显式转换器进行自定义。

JSR-310 日期/时间类型

直通


可以使用显式转换器进行自定义。

​String​​​和​​BigInteger​​​​BigDecimal​​​​UUID​

直通

可以使用显式转换器进行自定义。

​Enum​

字符串

可以通过注册显式转换器进行自定义。

​Blob​​​和​​Clob​

直通

可以使用显式转换器进行自定义。

​byte[]​​​, ​​ByteBuffer​

直通

被视为二进制有效负载。

​Collection<T>​

数组​​T​

转换为数组类型(如果配置的​​驱动程序​​支持),否则不支持。

基元类型、包装器和​​String​

包装器类型的数组(例如→​​int[]​​​​Integer[]​​)

转换为数组类型(如果配置的​​驱动程序​​支持),否则不支持。

特定于驱动程序的类型

直通

由使用的简单类型贡献。​​R2dbcDialect​

复杂对象

目标类型取决于已注册。​​Converter​

需要显式转换器,否则不受支持。

列的本机数据类型取决于 R2DBC 驱动程序类型映射。 驱动程序可以贡献其他简单类型,例如几何类型。

16.4.2. 映射注释概述

可以使用元数据来驱动对象到行的映射。 以下注释可用:​​MappingR2dbcConverter​

  • ​@Id​​:在字段级别应用以标记主键。
  • ​@Table​​:在类级别应用,以指示此类是映射到数据库的候选项。 可以指定存储数据库的表的名称。
  • ​@Transient​​:默认情况下,所有字段都映射到该行。 此批注将应用该批注的字段排除在数据库中的存储之外。 不能在持久性构造函数中使用瞬态属性,因为转换器无法具体化构造函数参数的值。
  • ​@PersistenceConstructor​​:标记给定的构造函数(甚至是受包保护的构造函数),以便在从数据库中实例化对象时使用。 构造函数参数按名称映射到检索到的行中的值。
  • ​@Value​​:此注释是 Spring 框架的一部分。 在映射框架内,它可以应用于构造函数参数。 这允许您使用 Spring 表达式语言语句来转换数据库中检索到的键值,然后再将其用于构造域对象。 为了引用给定行的列,必须使用这样的表达式:where root 引用给定的根。@Value("#root.myProperty")Row
  • ​@Column​​:在字段级别应用,用于描述列在行中表示的名称,使名称与类的字段名称不同。 在 SQL 语句中使用注释指定的名称时,始终用引号括起来。 对于大多数数据库,这意味着这些名称区分大小写。 这也意味着您可以在这些名称中使用特殊字符。 但是,不建议这样做,因为它可能会导致其他工具出现问题。@Column
  • ​@Version​​:在字段级别应用用于乐观锁定,并检查保存操作的修改。 值 is(对于基元类型)被视为新实体的标记。 初始存储的值为 (对于基元类型)。 版本在每次更新时都会自动递增。 有关进一步参考,请参阅乐观锁定。nullzerozeroone

映射元数据基础结构在与技术无关的单独项目中定义。 R2DBC 支持中使用特定的子类来支持基于注释的元数据。 其他策略也可以到位(如果有需求)。​​spring-data-commons​

16.4.3. 自定义对象构造

映射子系统允许通过使用注释注释构造函数来自定义对象构造。要用于构造函数参数的值按以下方式解析:​​@PersistenceConstructor​

  • 如果使用注释对参数进行注释,则会计算给定的表达式,并将结果用作参数值。@Value
  • 如果 Java 类型的属性名称与输入行的给定字段匹配,则其属性信息用于选择要将输入字段值传递给的相应构造函数参数。 仅当 Javafiles 中存在参数名称信息时,这才有效,您可以通过使用调试信息编译源代码或使用 Java 8 中的命令行开关来实现。.class-parametersjavac
  • 否则,抛出 ais 以指示无法绑定给定的构造函数参数。MappingException
class OrderItem {

private @Id final String id;
private final int quantity;
private final double unitPrice;

OrderItem(String id, int quantity, double unitPrice) {
this.id = id;
this.quantity = quantity;
this.unitPrice = unitPrice;
}

// getters/setters ommitted
}

16.4.4. 使用显式转换器覆盖映射

在存储和查询对象时,让 a instance 处理所有 Java 类型到实例的映射通常很方便。 但是,您有时可能希望实例完成大部分工作,但允许您有选择地处理特定类型的转换 - 也许是为了优化性能。​​R2dbcConverter​​​​OutboundRow​​​​R2dbcConverter​

要自己有选择地处理转换,请向 注册一个或多个一个或多个实例。​​org.springframework.core.convert.converter.Converter​​​​R2dbcConverter​

您可以使用该方法配置转换器。 本章开头的示例显示了如何使用 Java 执行配置。​​r2dbcCustomConversions​​​​AbstractR2dbcConfiguration​

自定义*实体转换需要非对称类型进行转换。 入站数据是从R2DBC中提取的。 出站数据(与 /语句一起使用)表示为稍后组装到语句中。​​Row​​​​INSERT​​​​UPDATE​​​​OutboundRow​

以下弹簧转换器实现示例从 ato aPOJO 转换:​​Row​​​​Person​

@ReadingConverter
public class PersonReadConverter implements Converter<Row, Person> {

public Person convert(Row source) {
Person p = new Person(source.get("id", String.class),source.get("name", String.class));
p.setAge(source.get("age", Integer.class));
return p;
}
}

请注意,转换器适用于奇异属性。 集合属性(例如)按元素进行迭代和转换。 不支持集合转换器(例如)。​​Collection<Person>​​​​Converter<List<Person>>, OutboundRow​

R2DBC 使用盒装基元(而不是)返回基元值。​​Integer.class​​​​int.class​

以下示例从 ato a 转换:​​Person​​​​OutboundRow​

@WritingConverter
public class PersonWriteConverter implements Converter<Person, OutboundRow> {

public OutboundRow convert(Person source) {
OutboundRow row = new OutboundRow();
row.put("id", Parameter.from(source.getId()));
row.put("name", Parameter.from(source.getFirstName()));
row.put("age", Parameter.from(source.getAge()));
return row;
}
}
使用显式转换器覆盖枚举映射

某些数据库(如Postgres)可以使用其特定于数据库的枚举列类型本机写入枚举值。 Spring Data 默认将值转换为值,以实现最大的可移植性。 若要保留实际枚举值,请注册其源和目标类型使用实际枚举类型的转换器,以避免使用转换。 此外,需要在驱动程序级别配置枚举类型,以便驱动程序知道如何表示枚举类型。​​Enum​​​​String​​​​@Writing​​​​Enum.name()​

以下示例显示了本机读取和写入值所涉及的组件:​​Color​

enum Color {
Grey, Blue
}

class ColorConverter extends EnumWriteSupport<Color> {

}


class Product {
@Id long id;
Color color;

// …
}

17. Kotlin 支持

Kotlin是一种针对 JVM(和其他平台)的静态类型语言,它允许编写简洁优雅的代码,同时提供与用 Java 编写的现有库的出色互操作性。

Spring Data 为 Kotlin 提供了一流的支持,并允许开发人员编写 Kotlin 应用程序,就好像 Spring Data 是 Kotlin 原生框架一样。

使用 Kotlin 构建 Spring 应用程序的最简单方法是利用 Spring Boot 及其专用的 Kotlin 支持。 这个全面的教程将教你如何使用start.spring.io 使用 Kotlin 构建 Spring Boot 应用程序。

17.1. 要求

Spring Data 支持 Kotlin 1.3,并要求 kotlin-stdlib(或其变体之一,如kotlin-stdlib-jdk8)和kotlin-reflect 出现在类路径上。 如果您通过start.spring.io 引导 Kotlin 项目,则默认提供这些。

17.2. 空安全

Kotlin 的主要特性之一是空安全,它在编译时干净地处理值。 这通过可空性声明和“值或无值”语义的表达式使应用程序更安全,而无需支付包装器的成本,例如。 (Kotlin 允许使用具有可为空值的函数构造。请参阅此Kotlin 零安全综合指南。nullOptional

虽然Java不允许你在其类型系统中表达空安全,但Spring Data API是用JSR-305工具友好的注解来注释的。 默认情况下,Kotlin 中使用的 Java API 中的类型被识别为平台类型,其空检查被放宽。Kotlin 对 JSR-305 注释和 Spring 可空性注释的支持为 Kotlin 开发人员提供了整个 Spring 数据 API 的空安全性,具有在编译时处理相关问题的优势。org.springframework.langnull

请参阅存储库方法的空处理如何将空安全性应用于 Spring 数据存储库。


您可以通过使用以下选项添加编译器标志来配置 JSR-305 检查:​​-Xjsr305​​​​-Xjsr305={strict|warn|ignore}​



对于 Kotlin 版本 1.1+,默认行为与 相同。 该值是必需的,考虑到Spring Data API null-safety。Kotlin 类型是从 Spring API 推断出来的,但在使用 Spring API 可空性声明时,即使在次要版本之间也是如此,并且将来可能会添加更多检查。​​-Xjsr305=warn​​​​strict​


尚不支持泛型类型参数、varargs 和数组元素 nullability,但应该在即将发布的版本中支持。

17.3. 对象映射

请参阅 Kotlin支持,了解有关 Kotlin对象如何具体化的详细信息。

17.4. 扩展

Kotlin扩展提供了使用附加功能扩展现有类的能力。Spring Data Kotlin API 使用这些扩展为现有的 Spring API 添加新的特定于 Kotlin 的便利。


请记住,需要导入 Kotlin 扩展才能使用。 与静态导入类似,在大多数情况下,IDE 应自动建议导入。


例如,Kotlin 化类型参数为 JVM泛型类型擦除提供了一种解决方法,Spring Data 提供了一些扩展来利用此功能。 这允许更好的 Kotlin API。

要在 Java 中检索对象列表,您通常需要编写以下内容:​​SWCharacter​

Flux<SWCharacter> characters = client.select().from(SWCharacter.class).fetch().all();

使用 Kotlin 和 Spring Data 扩展,您可以编写以下内容:

val characters =  client.select().from<SWCharacter>().fetch().all()
// or (both are equivalent)
val characters : Flux<SWCharacter> = client.select().from().fetch().all()

与Java一样,Kotlin是强类型的,但Kotlin的聪明类型推断允许更短的语法。​​characters​

Spring Data R2DBC提供以下扩展:

  • 化泛型支持和。DatabaseClientCriteria
  • 协程扩展。DatabaseClient

17.5. 协程

Kotlin协程是轻量级线程,允许命令性地编写非阻塞代码。 在语言端,函数为异步操作提供了抽象,而在库端,kotlinx.coroutines提供了像async { } 这样的函数和像Flow 这样的类型。suspend

Spring 数据模块在以下范围内为协程提供支持:

  • Kotlin 扩展中的延迟和流返回值支持

17.5.1. 依赖关系

当 和依赖项位于类路径中时,将启用协程支持:​​kotlinx-coroutines-core​​​​kotlinx-coroutines-reactive​​​​kotlinx-coroutines-reactor​

例 83。要在 Maven pom 中添加的依赖项.xml

<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
</dependency>

<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactive</artifactId>
</dependency>

<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
</dependency>

支持的版本及以上版本。​​1.3.0​

17.5.2. 响应式如何转换为协程?

对于返回值,从反应式 API 到协程 API 的转换如下所示:

  • ​fun handler(): Mono<Void>​​成为suspend fun handler()
  • ​fun handler(): Mono<T>​​变得取决于是否可以为空(具有更静态类型的优点)suspend fun handler(): Tsuspend fun handler(): T?Mono
  • ​fun handler(): Flux<T>​​成为fun handler(): Flow<T>

流在协程世界中是等效的,适用于热流或冷流、有限流或无限流,主要区别如下:​​Flux​

  • ​Flow​​是基于推的,而是推挽混合的Flux
  • 通过悬挂功能实现背压
  • ​Flow​​只有一个挂起收集方法,运算符作为扩展实现
  • 借助协程,运算符易于实现
  • 扩展允许将自定义运算符添加到Flow
  • 收集操作正在暂停功能
  • map运算符支持异步操作(不需要),因为它需要一个挂起函数参数flatMap

阅读这篇关于使用Spring、协程和 Kotlin Flow 进行响应式的博客文章,了解更多详细信息,包括如何与协程同时运行代码。

17.5.3. 仓库

下面是协程存储库的示例:

interface CoroutineRepository : CoroutineCrudRepository<User, String> {

suspend fun findOne(id: String): User

fun findByFirstname(firstname: String): Flow<User>

suspend fun findAllByFirstname(id: String): List<User>
}

协程存储库建立在反应式存储库之上,通过 Kotlin 的协程公开数据访问的非阻塞性质。 协程存储库上的方法可以由查询方法或自定义实现提供支持。 如果自定义方法无需实现方法返回反应类型(如 asor),则调用自定义实现方法会将协程调用传播到实际实现方法。​​suspend​​​​Mono​​​​Flux​

请注意,根据方法声明,协程上下文可能可用,也可能不可用。 若要保留对上下文的访问权限,请声明方法using或返回启用上下文传播的类型,例如。​​suspend​​​​Flow​

  • ​suspend fun findOne(id: String): User​​:通过挂起同步检索数据一次。
  • ​fun findByFirstname(firstname: String): Flow<User>​​​:检索数据流。 在交互时获取数据时急切地创建 ()。​​​Flow.collect(…)​
  • ​fun getUser(): User​​:在阻塞线程且没有上下文传播的情况下检索数据。 应避免这种情况。

仅当存储库扩展接口时,才会发现协程存储库。​​CoroutineCrudRepository​