前言
Hibernate Reactive 是 Hibernate ORM 的一个反应式 API,它支持非阻塞数据库驱动程序和与数据库的反应式交互方式。
Hibernate Reactive 旨在用于 Vert.x 等反应式编程环境,其中与数据库的交互应以非阻塞方式发生。持久化操作是通过构建反应式流来协调的,而不是通过在过程式 Java 代码中直接调用同步函数来协调。反应式流使用 Java CompletionStage
或 Mutiny Uni
和 Multi
的链来表示。
JDBC、JPA 和 Hibernate ORM 等 Java 持久化框架旨在使用阻塞 IO 与数据库交互,因此不适合用于反应式环境。据我们所知,Hibernate Reactive 是第一个真正旨在利用非阻塞数据库客户端的 ORM 实现。开箱即用,支持 PostgreSQL、MySQL、DB2、SQL Server、Oracle 和 CockroachDB 的 Vert.x 客户端,尽管架构不限于这些驱动程序。
这种编程范式有可能在某些运行时场景中提高可扩展性和更受控的峰值负载退化。但是,通常情况下,不应期望在所有性能测试中立即看到性能提升。实际上,许多程序将**不会**从编程模型中获益,而那些确实获益的程序可能只会在非常具体的负载场景中获益。
1. Hibernate Reactive 简介
使用 Hibernate Reactive 创建一个新项目并不难。在本简短指南中,我们将介绍在以下方面进行的所有基本工作
-
设置和配置项目,然后
-
编写 Java 代码来定义数据模型并访问数据库。
最后,我们将讨论一些与性能相关的主题,在使用 Hibernate 开发任何大型项目时,您需要了解这些主题。
但是,在开始之前,我们建议您快速查看 session-example
目录中非常简单的示例程序,它展示了使您自己的程序运行所需的所有“内容”。
1.1. 关于 Hibernate ORM 的信息
本文档假定您对 Hibernate ORM 或 JPA 的其他实现有一些了解。如果您以前从未使用过 JPA,没关系,但您可能需要在本文档的某些部分参考以下信息源
-
使用 Hibernate 进行 Java 持久化,最初名为Hibernate in Action的书籍的最新版本。
1.2. 设置一个反应式 Hibernate 项目
如果您在 Quarkus 环境之外使用 Hibernate Reactive,则需要
-
将 Hibernate Reactive 本身以及相应的 Vert.x 反应式数据库客户端作为项目的依赖项包含在内,以及
-
使用 Hibernate 配置属性使用有关数据库的信息配置 Hibernate Reactive。
或者,如果您想在 Quarkus 中使用 Hibernate Reactive,您可以生成一个预配置的骨架项目就在这里.
1.2.1. 在您的项目构建中包含 Hibernate Reactive
将以下依赖项添加到您的项目
org.hibernate.reactive:hibernate-reactive-core:{version}
其中 {version}
是您正在使用的 Hibernate Reactive 版本。
您还需要为数据库添加 Vert.x 反应式数据库驱动程序的依赖项,以下列出了一些选项
数据库 | 驱动程序依赖项 |
---|---|
PostgreSQL 或 CockroachDB |
|
MySQL 或 MariaDB |
|
DB2 |
|
SQL Server |
|
Oracle |
|
其中 {vertxSqlClientVersion}
是与您正在使用的 Hibernate Reactive 版本兼容的 Vert.x 版本。
您不需要依赖数据库的 JDBC 驱动程序。
1.2.2. 可选依赖项
可选地,您还可以添加以下任何其他功能
可选功能 | 依赖项 |
---|---|
一个 SLF4J 日志记录实现 |
|
Hibernate 元模型生成器(如果您使用 JPA 标准查询 API) |
|
Hibernate Validator |
|
对您的 HQL 查询进行编译时检查 |
|
通过 JCache 和 EHCache 支持二级缓存 |
|
对 PostgreSQL 的 SCRAM 身份验证支持 |
|
如果您想使用域级延迟获取,您还可以将 Hibernate 字节码增强器 添加到您的 Gradle 构建中。
域级延迟获取是一项高级功能,大多数程序不需要。现在先坚持使用基本功能。 |
示例程序中包含一个示例 Gradle 构建。
1.2.3. 基本配置
Hibernate Reactive 通过标准 JPA persistence.xml
文档进行配置,该文档必须像往常一样放置在 /META-INF
目录中。
示例程序中包含一个示例 persistence.xml
文件。
唯一真正特定于 Hibernate Reactive 的必需配置是持久性 <provider>
元素,它必须是显式的
<provider>org.hibernate.reactive.provider.ReactivePersistenceProvider</provider>
否则,配置几乎完全透明——您可以像通常配置 Hibernate ORM 核心一样配置 Hibernate Reactive。
就像在常规 JPA 中一样,您应该在 persistence.xml
中列出您的实体类
<class>org.hibernate.reactive.example.session.Author</class>
<class>org.hibernate.reactive.example.session.Book</class>
可以在 Hibernate ORM 文档 中找到 Hibernate 识别的所有配置属性的完整列表。您永远不需要触摸这些属性中的大多数。您在此阶段需要的属性是以下三个
配置属性名称 | 用途 |
---|---|
|
数据库的 JDBC URL |
|
您的数据库凭据 |
这些配置属性的名称中包含 jdbc
,但当然 Hibernate Reactive 中没有 JDBC,这些只是 JPA 规范定义的传统属性名称。特别是,Hibernate Reactive 本身会解析和解释 JDBC URL。
您不需要指定 hibernate.dialect 。Hibernate Reactive 将为您确定正确的 Hibernate Dialect 。 |
Vert.x 数据库客户端内置连接池和准备语句缓存。您可能想控制连接池的大小
配置属性名称 | 用途 |
---|---|
|
反应式连接池的最大大小 |
我们将在后面学习有关更高级的连接池调整,在 调整 Vert.x 池 中。
Hibernate 具有许多可配置的选项,但许多选项只存在是为了保持与传统代码的兼容性,并且大多数与 JDBC 或 JTA 直接相关的配置属性在 Hibernate Reactive 的上下文中不相关。 |
1.2.4. 自动模式导出
您可以让 Hibernate Reactive 从您在 Java 代码中指定的映射注释中推断您的数据库模式,并在初始化时导出模式,方法是指定以下一个或多个配置属性
配置属性名称 | 用途 |
---|---|
|
|
|
(可选)如果为 |
|
(可选)如果为 |
|
(可选)要执行的 SQL 脚本的名称 |
此功能对于测试非常有用。
Hibernate Reactive 不支持使用 Db2 的 validate 和 update 。 |
模式导出使用阻塞操作,因此在使用它时,启动工厂可能需要特殊处理。未能这样做会导致异常
io.vertx.core.VertxException: Thread blocked
您可以使用 executeBlocking
解决此问题
Vertx vertx = ...
Uni<Void> startHibernate = Uni.createFrom().deferred(() -> {
emf = Persistence
.createEntityManagerFactory("demo")
.unwrap(Mutiny.SessionFactory.class);
return Uni.createFrom().voidItem();
});
startHibernate = vertx.executeBlocking(startHibernate)
.onItem().invoke(() -> logger.info("✅ Hibernate Reactive is ready"));
1.2.5. 记录生成的 SQL
要查看发送到数据库的生成的 SQL,可以执行以下操作
-
将属性
hibernate.show_sql
设置为true
,或 -
使用您首选的 SLF4J 日志记录实现,为类别 `org.hibernate.SQL` 启用调试级别日志记录。
例如,如果您使用的是 Log4J 2(如上文中的 可选依赖项),请将以下行添加到您的 `log4j2.properties` 文件中
logger.hibernate.name = org.hibernate.SQL
logger.hibernate.level = debug
示例 `log4j2.properties` 文件包含在示例程序中。
您可以通过启用以下一个或两个设置来使记录的 SQL 更具可读性
配置属性名称 | 用途 |
---|---|
|
如果为 `true`,则以多行、缩进格式记录 SQL |
|
如果为 `true`,则使用 ANSI 转义码以语法突出显示的方式记录 SQL |
1.2.6. 最小化重复的映射信息
以下属性对于最小化您需要在 `@Table` 和 `@Column` 注释中显式指定的 信息量非常有用,我们将在下面 映射实体类 中讨论。
配置属性名称 | 用途 |
---|---|
|
对于没有显式声明架构的实体的默认架构名称 |
|
对于没有显式声明目录的实体的默认目录名称 |
|
一个实现您数据库命名标准的 `PhysicalNamingStrategy` |
编写您自己的 `PhysicalNamingStrategy` 是减少实体类注释混乱的特别好方法,我们认为您应该为任何非平凡的数据模型执行此操作。 |
1.3. 编写 Java 代码
现在,我们已经准备好编写一些 Java 代码了!
与任何使用 Hibernate 的项目一样,您的持久性相关代码分为两个主要部分
-
您在 Java 中的数据模型表示,它采用一组带注释的实体类的形式,以及
-
大量与 Hibernate API 交互以执行与您的各种事务相关的持久性操作的功能。
第一部分,数据或“域”模型,通常更容易编写,但是,完成一项出色且非常干净的工作将极大地影响您在第二部分中的成功。
花时间编写此代码,并尝试生成一个尽可能接近关系数据模型的 Java 模型。在没有真正需要的情况下,避免使用奇特或高级的映射功能。如果有一丝疑问,请使用 `@ManyToOne` 和 `@OneToMany(mappedBy=…)` 映射外键关系,而不是更复杂的关联映射。 |
代码的第二部分要难得多。此代码必须
-
管理事务和响应式会话,
-
通过将对响应式会话调用的持久性操作链接起来,构建响应式流,
-
获取和准备 UI 所需的数据,以及
-
处理失败。
某些事务和会话管理的责任,以及从某些类型的故障中恢复的责任,最好在某种框架代码中处理。 |
1.3.1. 映射实体类
我们在这里不会过多地谈论实体类,仅仅是因为 Hibernate Reactive 中映射实体类的原则,以及您将使用的实际映射注释,都与常规 Hibernate ORM 和 JPA 的其他实现完全相同。
例如
@Entity
@Table(name="authors")
class Author {
@Id @GeneratedValue
private Integer id;
@NotNull @Size(max=100)
private String name;
@OneToMany(mappedBy = "author", cascade = PERSIST)
private List<Book> books = new ArrayList<>();
Author(String name) {
this.name = name;
}
Author() {}
// getters and setters...
}
您可以自由地混合和匹配
-
包 `jakarta.persistence.` 中定义的常规 JPA 映射注释,以及
-
org.hibernate.annotations
中的高级映射注释,甚至 -
像 `@NotNull` 和 `@Size` 这样的注释,这些注释由 Bean Validation 定义。
有关对象/关系映射注释的完整列表,请参阅 Hibernate ORM 文档。Hibernate Reactive 已经支持大多数映射注释,尽管目前仍存在一些限制。
常见的 JPA 注释
最常见和最有用的映射注释包括以下标准 JPA 注释
注释 | 用途 |
---|---|
|
声明一个实体类(一个具有自己的数据库表和持久性标识的类) |
|
一个超类,声明其 `@Entity` 子类的通用持久性字段 |
|
声明一个可嵌入类(一个没有自己的持久性标识或数据库表的类) |
|
定义如何将继承层次结构映射到数据库表 |
|
指定实体的字段保存实体的持久性标识,并映射到其表的主键 |
|
指定一个类来表示实体的组合主键(对于具有多个 `@Id` 字段的实体) |
|
指定实体的字段保存其组合主键,该主键表示为一个 `@Embeddable` 类 |
|
指定标识符是系统生成的代理键 |
|
指定实体的字段保存用于乐观锁定的版本号 |
|
映射保存 `enum` 的字段 |
|
声明对第二个实体的多对一关联 |
|
声明对第二个实体的一对一关联 |
|
声明对第二个实体的一对多关联 |
|
指定对数据库表的映射 |
|
指定对第二个数据库表的映射 |
|
指定对数据库列的映射 |
|
指定对数据库外键的映射 |
有用的 Hibernate 注释
了解以下 Hibernate 注释也非常有用
注释 | 用途 |
---|---|
|
为实体启用二级缓存 |
|
将字段映射到 SQL 表达式,而不是列 |
|
自动将时间戳分配给字段 |
|
为没有 `@Version` 字段的实体启用乐观锁定 |
|
定义 Hibernate 过滤器 |
|
定义 Hibernate 获取配置文件 |
|
定义由数据库生成的属性 |
|
指定用于为列分配默认值的 SQL 表达式(与 `@Generated(INSERT)` 结合使用) |
|
选择自定义 ID 生成器 |
|
动态生成 SQL,只使用必要的列(而不是使用启动时生成的静态 SQL) |
|
指定关联的获取模式 |
|
指定批量获取关联的批量大小 |
|
指定用于通过 ID 获取实体的命名查询(例如,当调用 `find(type, id)` 时),以代替 Hibernate 生成的默认 SQL |
|
指定实体操作的自定义 DML |
|
将字段或字段标记为实体的备用“自然”标识符(唯一键) |
|
对特定列选择性地使用 `nchar`、`nvarchar` 或 `nclob`。 |
|
指定实体或集合是不可变的 |
|
映射 `SortedSet` 或 `SortedMap` |
|
声明要添加到 DDL 的 SQL `check` 约束 |
Bean Validation 注释
有关 Bean Validation 注释的信息,请参阅 Hibernate Validator 文档。
对于定义必需字段,我们更喜欢使用来自 Bean Validation 的 `@NotNull` 注释,而不是 JPA 更冗长的 `@Basic(optional=false)`。同样,我们更喜欢使用 `@Size(100)` 而不是 `@Column(length=100)` 来定义文本字段的长度。 |
1.3.2. getter 和 setter
在非 Quarkus 环境中使用 Hibernate Reactive 时,您需要按照通常的 JPA 约定编写实体类,这些约定要求
-
持久性属性的私有字段,以及
-
无参构造函数。
从实体类外部访问持久性字段是非法的。因此,对持久性字段的外部访问必须通过实体类定义的 getter 和 setter 方法来中介。
如果您从实体类外部的代码访问未获取的实体实例的字段,您将获得虚假的 `null` 或默认值(零)! |
当您在 Quarkus 中使用 Hibernate Reactive 时,这些要求会放宽,如果您愿意,可以使用公共字段而不是 getter 和 setter。
1.3.3. `equals()` 和 `hashCode()`
实体类应该重写 `equals()` 和 `hashCode()`。Hibernate 或 JPA 的新手经常对 `hashCode()` 中应该包含哪些字段感到困惑,因此请牢记以下原则
-
您不应该将可变字段包含在哈希码中,因为这将需要在每次修改字段时重新哈希包含该实体的任何集合。
-
将生成的标识符(代理键)包含在哈希码中并不完全错误,但由于标识符是在实体实例变得持久之前生成的,因此您必须格外小心,不要在生成标识符之前将其添加到任何哈希集合中。因此,我们建议不要将任何数据库生成的字段包含在哈希码中。
将任何不可变的、非生成的字段包含在哈希码中是可以的。
因此,我们建议为每个实体确定一个自然键,即从程序的数据模型的角度来看,唯一标识实体实例的字段组合。业务键应该对应于数据库上的唯一约束,以及包含在 `equals()` 和 `hashCode()` 中的字段。 |
也就是说,基于实体的生成标识符的 `equals()` 和 `hashCode()` 的实现可以在您小心的情况下起作用。
如果您无法确定自然键,这可能表明您需要更仔细地考虑数据模型的某些方面。如果实体没有有意义的唯一键,那么就不可能说出它在程序之外的“现实世界”中代表什么事件或对象。 |
请注意,即使您已经确定了自然键,我们仍然建议在外键中使用生成的代理键,因为这会使您的数据模型更加容易更改。
1.3.4. 标识符生成
Hibernate Reactive 的功能与普通 Hibernate 偏离的一个领域是 ID 生成领域。为与 Hibernate ORM 和 JDBC 一起使用而编写的自定义标识符生成器将无法在响应式环境中使用。
-
序列、表和 `UUID` ID 生成是内置的,可以使用通常的 JPA 映射注释选择这些 ID 生成策略:`@GeneratedValue`、`@TableGenerator`、`@SequenceGenerator`。
-
在 MySQL 上,可以通过指定 `@GeneratedValue(strategy=GenerationType.IDENTITY)` 来使用自动递增列
-
自定义 ID 生成器可以通过实现 `ReactiveIdentifierGenerator` 并使用 `@GenericGenerator` 声明自定义实现来定义。
-
自然 ID(包括组合 ID)可以通过程序以通常的方式分配。
JPA 规范定义的标准 ID 生成策略可以通过以下注解进行自定义
注释 | 用途 |
---|---|
|
配置基于数据库序列的生成器 |
|
配置基于数据库表行的生成器 |
例如,序列 ID 生成可以这样指定
@Entity
@Table(name="authors")
class Author {
@Id @GeneratedValue(generator = "authorIds")
@SequenceGenerator(name = "authorIds",
sequenceName = "author_ids",
allocationSize = 20)
Integer id;
...
}
您可以在 JPA 规范中找到更多信息。
如果您有非常特殊的需求,可以查看 ReactiveIdentifierGenerator
的 Javadoc,了解如何实现您自己的自定义反应式标识符生成器。
1.3.5. 自定义类型
基于 UserType
接口的 Hibernate 自定义类型针对与 JDBC 一起使用,并且依赖于 JDBC 定义的接口。因此,Hibernate Reactive 提供了一个适配器,它向 UserType
实现公开 JDBC 的部分实现。
因此,某些现有的 UserType
实现将在 Hibernate Reactive 中起作用,具体取决于它们依赖的 JDBC 功能。
如果可能,请使用 JPA 属性转换器而不是自定义类型,因为属性转换器与 JDBC 没有任何关联。 |
您可以通过使用 Hibernate @Type
注解来注释实体类的一个字段,来指定自定义类型。
1.3.6. 属性转换器
任何 JPA AttributeConverter
都可以在 Hibernate Reactive 中使用。例如
@Converter
public class BigIntegerAsString implements AttributeConverter<BigInteger, String> {
@Override
public String convertToDatabaseColumn(BigInteger attribute) {
return attribute == null ? null : attribute.toString(2);
}
@Override
public BigInteger convertToEntityAttribute(String string) {
return string == null ? null : new BigInteger(string, 2);
}
}
您需要使用以下一个或两个注解
注释 | 用途 |
---|---|
|
声明一个实现 |
|
指定要用于实体类字段的 |
您可以在这些注解的 Javadoc 和 JPA 规范中找到更多信息。
1.3.7. 用于链接反应式操作的 API
当您使用 Hibernate Reactive 编写持久化逻辑时,您大多数时候都在使用反应式 Session
。为了让新用户更加困惑,反应式 Session
及其相关接口都有两种形式
-
Stage.Session
及其朋友提供了一个基于 JavaCompletionStage
的反应式 API,以及 -
Mutiny.Session
及其朋友提供了一个基于 Mutiny 的 API。
您需要决定要使用哪个 API!
如果您花时间查看 Stage.Session 和 Mutiny.Session 类型,您会注意到它们几乎完全相同。在它们之间进行选择只是决定您想要使用哪个反应式 API 来处理反应式流。您的决定不会影响您使用 Hibernate Reactive 的能力。另一方面,我们已经向 Mutiny 团队发送了许多反馈和改进请求,我们认为现在使用 Mutiny 的 Hibernate Reactive 代码更简单、更干净。 |
以下是在使用 Hibernate Reactive 时一直需要的反应式流上最重要的操作
用途 | Java CompletionStage |
Mutiny Uni |
---|---|---|
链接非阻塞操作 |
|
|
转换流式传输的项 |
|
|
使用流式传输的项执行操作 |
|
|
执行清理(类似于 |
|
|
在本介绍中,我们的代码示例通常使用 Mutiny。如果您更熟悉 CompletionStage
,您可以参考上表来帮助您理解代码。
当我们在本文档中使用术语反应式流时,我们的意思是
-
一系列
CompletionStage
,或 -
一系列 Mutiny
Uni
和Multi
它是为了服务特定请求、事务或工作单元而由程序构建的。
1.3.8. 获取反应式会话工厂
无论您决定什么,获取反应式会话的第一步是获取 JPA EntityManagerFactory
,就像您通常在普通的 JPA 中一样,例如,通过调用
EntityManagerFactory emf = Persistence.createEntityManagerFactory("example");
现在,unwrap()
反应式 SessionFactory
。如果您想使用 CompletionStage
来链接反应式操作,请请求 Stage.SessionFactory
Stage.SessionFactory sessionFactory = emf.unwrap(Stage.SessionFactory.class);
或者,如果您更喜欢使用基于 Mutiny 的 API,则 unwrap()
类型 Mutiny.SessionFactory
Mutiny.SessionFactory sessionFactory = emf.unwrap(Mutiny.SessionFactory.class);
可以从生成的反应式 SessionFactory
获取反应式会话。
也可以通过基于 Hibernate 的 ServiceRegistry 架构的编程配置来构建反应式 SessionFactory ,方法是使用 ReactiveServiceRegistryBuilder 。但这超出了本文档的范围。 |
1.3.9. 获取反应式会话
持久化操作通过反应式 Session
对象公开。了解此接口的大多数操作都是非阻塞的,并且永远不会同步执行针对数据库的 SQL 至关重要。属于单个工作单元的持久化操作必须在单个反应式流内通过组合进行链接。
还要记住,Hibernate 会话是一个轻量级对象,应该在单个逻辑工作单元内创建、使用,然后丢弃。
也就是说,您应该在单个反应式流中代表特定事务或工作单元的多个持久化操作之间重用同一个会话,但不要在不同的并发反应式流之间共享会话! |
要从 SessionFactory
获取反应式 Session
,请使用 withSession()
sessionFactory.withSession(
session -> session.find(Book.class, id)
.invoke(
book -> ... //do something with the book
)
);
生成的 Session
对象会自动与当前反应式流关联,因此在给定流中嵌套调用 withSession()
会自动获取相同的共享会话。
或者,您可以使用 openSession()
,但您必须记住在完成时 close()
会话。并且您必须非常小心,只能从一个 Vert.x 上下文内访问每个会话。(有关更多信息,请参阅 会话和 Vert.x 上下文)。
Uni<Session> sessionUni = sessionFactory.openSession();
sessionUni.chain(
session -> session.find(Book.class, id)
.invoke(
book -> ... //do something with the book
)
.eventually(session::close)
);
1.3.10. 使用反应式会话
Session
接口具有与 JPA EntityManager
的方法同名的函数。您可能已经熟悉 JPA 定义的以下会话操作
方法名称和参数 | 效果 |
---|---|
|
根据其类型和 ID(主键)获取持久化对象 |
|
使瞬态对象持久化,并为以后执行安排 SQL |
|
使持久化对象瞬态,并为以后执行安排 SQL |
|
将给定分离对象的 state 复制到相应的受管持久化实例,并返回持久化对象 |
|
使用新的 SQL |
|
获取持久化对象的悲观锁 |
|
检测对与会话关联的持久化对象的更改,并通过执行 SQL |
|
在不影响数据库的情况下,将持久化对象与会话分离 |
|
获取对持久化对象的引用,而不实际从数据库加载其 state |
如果您不熟悉这些操作,请不要绝望!它们的语义在 JPA 规范和 API 文档中定义,并在无数文章和博文中进行了说明。但是,如果您已经有一些使用 Hibernate 或 JPA 的经验,那么您就来了!
就像在 Hibernate ORM 中一样,如果会话的任何方法抛出异常,则会话被认为是不可用的。如果您从 Hibernate Reactive 收到异常,您应该立即关闭并丢弃当前会话。 |
现在,这是 Hibernate Reactive 与众不同的地方:在反应式 API 中,这些方法中的每一个都通过 Java CompletionStage
(或 Mutiny Uni
)以非阻塞方式返回其结果。例如
session.find(Book.class, book.id)
.invoke( book -> System.out.println(book.title + " is a great book!") )
另一方面,没有有意义的返回值的方法只返回 CompletionStage<Void>
(或 Uni<Void>
)。
session.find(Book.class, id)
.call( book -> session.remove(book) )
.call( () -> session.flush() )
如果(并且仅当)您使用事务,会话将在工作单元结束时自动刷新,如下面的 事务 中所述。如果您不使用事务,并且忘记显式刷新会话,您的持久化操作可能永远不会发送到数据库! |
使用反应式流时的一个非常常见的错误是忘记链接“类 void”方法的返回值。例如,在以下代码中,flush()
操作从未执行,因为 invoke()
不会将其返回值链接到流的顶端。
session.find(Book.class, id)
.call( book -> session.remove(book) )
.invoke( () -> session.flush() ) //OOPS, WRONG!!
所以请记住
-
在调用返回
CompletionStage
的“类 void”方法时,您必须使用thenCompose()
,而不是thenAccept()
。 -
在 Mutiny 中,在调用返回
Uni
的“类 void”方法时,您必须使用call()
,而不是invoke()
。
以下代码中也存在相同的问题,但这次是 remove()
从未被调用
session.find(Book.class, id)
.call( book -> {
session.remove(book); //OOPS, WRONG!!
return session.flush();
} )
如果您已经有一些使用反应式编程的经验,那么这里没有什么新东西需要学习。但是,如果您是反应式编程的新手,请注意您将以某种形式至少犯一次这个错误!
1.3.11. 查询
自然,Session
接口是 Query
实例的工厂,它允许您设置查询参数并执行查询和 DML 语句
方法名称 | 效果 |
---|---|
|
获取用于执行以 HQL 或 JPQL 编写的查询或 DML 语句的 |
|
获取用于执行以数据库的本机 SQL 方言编写的查询或 DML 语句的 |
|
获取用于执行由 |
该 createQuery()
方法生成一个反应式 Query
,允许异步执行 HQL/JPQL 查询,始终通过 CompletionStage
(或 Uni
)返回其结果
session.createQuery("select title from Book order by title desc")
.getResultList()
.invoke( list -> list.forEach(System.out::println) )
Query
接口定义了以下重要操作
方法名称 | 效果 |
---|---|
|
设置查询参数的参数 |
|
限制查询返回的结果数量 |
|
指定要跳过的初始结果数量(用于结果分页) |
|
执行查询并获取单个结果 |
|
执行查询并将结果作为列表获取 |
|
执行 DML 语句并获取受影响的行数 |
Hibernate Reactive Query API 不支持 java.util.Date 或其在 java.sql 中的子类,也不支持 java.util.Calendar 。始终使用 java.time 类型(如 LocalDate 或 LocalDateTime )来指定对时间类型查询参数的参数。 |
对于 JPA 标准查询,您必须首先使用 SessionFactory.getCriteriaBuilder()
获取 CriteriaBuilder
,然后使用 Session.createQuery()
执行查询。
CriteriaQuery<Book> query = factory.getCriteriaBuilder().createQuery(Book.class);
Root<Author> a = query.from(Author.class);
Join<Author,Book> b = a.join(Author_.books);
query.where( a.get(Author_.name).in("Neal Stephenson", "William Gibson") );
query.select(b);
return session.createQuery(query).getResultList().invoke(
books -> books.forEach( book -> out.println(book.title) )
);
1.3.12. 获取延迟关联
在 Hibernate ORM 中,当第一次在会话内访问关联时,延迟关联会透明地获取。另一方面,在 Hibernate Reactive 中,延迟关联获取是一个异步过程,它通过 CompletionStage
(或 Uni
)生成结果。
因此,延迟获取是一个名为 fetch()
的显式操作,它是 Stage
和 Mutiny
的一个静态方法
session.find(Author.class, author.id)
.chain( author -> Mutiny.fetch(author.books) )
.invoke( books -> ... )
当然,如果您急切地获取关联,则无需执行此操作。
在将控制权传递给呈现 UI 的进程之前,确保您已获取所有所需数据非常重要!Hibernate Reactive 中没有透明的延迟获取,因此“视图中的打开会话”之类的模式将 *完全没有帮助*。 |
有时您可能需要链接对 fetch()
的多次调用,例如
Mutiny.fetch( session.getReference(detachedAuthor) )
.chain( author -> Mutiny.fetch(author.books) )
.invoke( books -> ... )
fetch() 不是递归的!在未先获取实体实例的情况下,您无法获取属于未获取实体的关联。 |
1.3.13. 字段级延迟获取
类似地,字段级延迟获取(一项高级功能,仅在与 Hibernate 的可选编译时字节码增强器结合使用时才支持)也是一项显式操作。
要声明延迟字段,我们通常使用 JPA @Basic
注释
@Basic(fetch=LAZY) String isbn;
声明为 @OneToOne(fetch=LAZY)
的可选一对一关联也被视为字段级延迟。
除非实体在构建期间由字节码增强器处理,否则此注释将完全无效。大多数 Hibernate 用户并不关心这一点,因为它通常很不方便。 |
另一方面,如果您在 Quarkus 中运行 Hibernate Reactive,则字节码增强器始终处于启用状态,您甚至不会注意到它的存在。 |
仅当我们通过调用 fetch()
操作的重载版本显式请求时,才会获取延迟字段
session.find(Book.class, book.id)
.chain( book -> session.fetch(book, Book_.isbn) )
.invoke( isbn -> ... )
请注意,要获取的字段由 JPA 元模型 Attribute
标识。
除非您有非常具体的需要,否则我们不建议您使用字段级延迟获取。一次获取实体的所有字段几乎总是更高效。字段级延迟获取与延迟关联获取一样容易受到 N+1 选择的影响。 |
1.3.14. 事务
withTransaction()
方法在数据库事务范围内执行工作。
session.withTransaction( tx -> session.persist(book) )
在事务结束时,会话会自动刷新。
对于给定的 Session
对象,对 withTransaction()
的嵌套调用发生在相同的共享事务上下文中。但是,请注意,事务仅是 *资源本地* 事务,委托给底层的 Vert.x 数据库客户端,并且不跨多个数据源,也不与 JPA 容器管理的事务集成。
Hibernate Reactive 目前不支持分布式 (XA) 事务。 |
为了方便起见,还有一个方法可以在一次调用中打开会话并启动事务
sessionFactory.withTransaction( (session, tx) -> session.persist(book) )
这可能是大多数情况下最方便使用的方法。
1.4. 集成 Vert.x
在运行时,与数据库的交互发生在 Vert.x 线程上,通常是事件循环线程。当您编写创建和销毁 Hibernate Reactive 会话的代码时,了解会话与线程和 Vert.x 上下文 的关系非常重要。
1.4.1. 会话和 Vert.x 上下文
请记住,在普通的旧 Hibernate JPA 中,您不应该在多个线程之间共享会话?嗯,这里的想法本质上是相似的,只是“线程”的概念有点难以捉摸,或者至少更 *技术性*。您需要能够将“线程”的概念替换为在特定 Vert.x *本地上下文* 范围内发生的反应流回调链的概念。 |
当您使用 withSession()
或 withTransaction()
创建会话时,它会自动与当前 Vert.x 本地上下文 关联,并在本地上下文中传播,如上所述 获取反应式会话。并且您只允许从拥有此本地上下文的线程使用会话。如果您搞砸了,并在不同的线程中使用它,您可能会看到此错误
HR000068: This method should exclusively be invoked from a Vert.x EventLoop thread; ...
另一方面,如果您使用 openSession()
,您将不得不自己管理会话和上下文之间的关联。现在,原则上这很简单,但您会惊讶于人们出错的频率。
Hibernate 会话不是线程安全的(也不是“流安全的”),因此在不同的线程(或反应流)之间使用它可能会导致 *非常* 难以检测的错误。不要说我们没有警告过你! |
许多用户对这种限制感到惊讶。但我们坚持认为这完全是自然的。从您作为会话用户角度来看,会话的原子操作是一个方法,例如 flush()
、find()
或 getResultList()
。任何一种方法都可能导致 *与数据库的多次交互*。在这些交互之间,会话根本没有处于定义良好的状态。反应流是一种线程,期望反应式编程在闪烁的魔法尘埃中消失您的并发问题是不合理的。这些事情不是这样运作的。
例如,我敢打赌您希望能够编写这样的代码
List<CompletionStage> list = ...
for (Entity entity : entities) {
list.add(session.persist(entity));
}
CompletableFuture.allOf(list).thenCompose(session::flush);
好吧,我们很抱歉,但这是不允许的。并行反应流不能共享会话。每个流必须有自己的会话。
1.4.2. 在 Vert.x 上下文中执行代码
如果您需要在 Vert.x 上下文的范围内运行一段代码,但当前线程未与 Context
关联怎么办?一种解决方案是使用 getOrCreateContext()
获取 Vert.x Context
对象,然后调用 runOnContext()
在该上下文中执行代码。
Context currentContext = Vertx.currentContext();
currentContext.runOnContext( event -> {
// Here you will be able to use the session
});
在传递给 runOnContext()
的代码块内,您将能够使用与上下文关联的 Hibernate Reactive 会话。
1.4.3. Vert.x 实例服务
VertxInstance
服务定义了 Hibernate Reactive 如何获取 Vert.x 的实例。默认实现只在第一次需要时创建它。但是,如果您的程序需要控制 Vert.x 实例的创建方式或获取方式,您可以覆盖默认实现并提供您自己的 VertxInstance
。让我们考虑这个例子
public class MyVertx implements VertxInstance {
private final Vertx vertx;
public MyVertx() {
this.vertx = Vertx.vertx();
}
@Override
public Vertx getVertx() {
return vertx;
}
}
注册此实现的一种方法是通过编程方式配置 Hibernate,例如
Configuration configuration = new Configuration();
StandardServiceRegistryBuilder builder = new ReactiveServiceRegistryBuilder()
.addService( VertxInstance.class, new MyVertx() )
.applySettings( configuration.getProperties() );
StandardServiceRegistry registry = builder.build();
SessionFactory sessionFactory = configuration.buildSessionFactory( registry );
或者,您可以实现 ServiceContributor
接口。
public class MyServiceContributor implements ServiceContributor {
@Override
public void contribute(StandardServiceRegistryBuilder serviceRegistryBuilder) {
serviceRegistryBuilder.addService( VertxInstance.class, new MyVertxProvider() );
}
}
要注册此 ServiceContributor
,请将名为 org.hibernate.service.spi.ServiceContributor
的文本文件添加到 /META-INF/services/
中。
org.myproject.MyServiceContributor
1.5. 调整和性能
一旦您使用 Hibernate Reactive 来访问数据库运行了一个程序,您不可避免地会发现性能令人失望或不可接受的地方。
幸运的是,大多数性能问题都比较容易解决,只要您牢记几个简单的原则,Hibernate 提供的工具就可以帮助您解决这些问题。
首先也是最重要的:您使用 Hibernate Reactive 的原因是它使事情变得更轻松。如果对于某个特定问题,它使事情变得 *更难*,请停止使用它。而是用其他工具解决这个问题。
仅仅因为您在程序中使用了 Hibernate,并不意味着您必须 *到处* 使用它。 |
其次:使用 Hibernate 的程序中,有两个主要潜在的性能瓶颈来源
-
对数据库的往返次数过多,以及
-
与一级(会话)缓存相关的内存消耗。
因此,性能调整主要涉及减少对数据库的访问次数,和/或控制会话缓存的大小。
但在我们进入这些更高级的话题之前,我们应该先调整连接池。
1.5.1. 调整 Vert.x 池
在 基本配置 中,我们已经看到了如何设置 Vert.x 数据库连接池的大小。在进行性能调整时,您可以通过以下配置属性进一步自定义池和预处理语句缓存
配置属性名称 | 用途 |
---|---|
|
等待队列中允许的最大连接请求 |
|
请求池化连接时等待的最大时间(毫秒) |
|
连接处于空闲状态的最大时间(毫秒) |
|
Vert.x 连接池清理器周期(毫秒) |
|
预处理语句缓存的最大大小 |
|
将被缓存的预处理语句 SQL 字符串的最大长度 |
最后,对于更高级的情况,您可以编写自己的代码来通过实现 SqlClientPoolConfiguration
来配置 Vert.x 客户端。
配置属性名称 | 用途 |
---|---|
|
实现 |
1.5.2. 启用语句批处理
几乎无需任何工作即可提高某些事务性能的一种简单方法是打开自动 DML 语句批处理。批处理仅在程序在单个事务中对同一表执行许多插入、更新或删除的情况下才有帮助。
您所需要做的就是设置一个属性
配置属性名称 | 用途 |
---|---|
|
SQL 语句批处理的最大批处理大小 |
(再次,此属性的名称中包含 jdbc
,但 Hibernate Reactive 将其重新用于反应式连接。)
比 DML 语句批处理更好的方法是使用 HQL update 或 delete 查询,甚至调用存储过程的原生 SQL! |
1.5.3. 关联获取
在 ORM 中实现高性能意味着最大限度地减少对数据库的往返次数。每当您使用 Hibernate 编写数据访问代码时,这个目标都应该牢记在心。ORM 中最基本的经验法则是
-
在会话/事务开始时明确指定您将需要的所有数据,并立即在一个或两个查询中获取它,
-
然后才开始在持久化实体之间导航关联。
毫无疑问,Java 程序中数据访问代码性能低下最常见的原因是 *N+1 选择* 问题。在这里,N 行的列表在初始查询中从数据库中检索,然后使用 N 个后续查询获取相关实体的关联实例。
执行此操作的 Hibernate 代码是错误的代码,并且让那些没有意识到这是他们自己没有遵循本节建议造成的的人认为 Hibernate 很糟糕! |
Hibernate 提供了几种策略来有效地获取关联并避免 N+1 选择
-
外连接获取,
-
批量获取,以及
-
子查询获取。
其中,您几乎应该始终使用外连接获取。批量获取和子查询获取仅在很少的情况下有用,在这种情况下,外连接获取会导致笛卡尔积和巨大的结果集。不幸的是,外连接获取与延迟获取根本无法实现。
避免使用延迟获取,它通常是 N+1 选择的来源。 |
从这个提示可以得出,您不应该经常使用 Stage.fetch()
或 Mutiny.fetch()
!
现在,我们并不是说关联应该默认映射为急切获取!那将是一个糟糕的主意,会导致简单的会话操作获取整个数据库!因此
大多数关联应该默认映射为延迟获取。 |
听起来这个提示与上一个提示相矛盾,但事实并非如此。它是在说,您必须在需要时,在需要的地方明确指定关联的急切获取。
如果您在某些特定事务中需要急切获取,请使用
-
left join fetch
在 HQL 中, -
获取配置文件,
-
JPA
EntityGraph
,或者 -
fetch()
在标准查询中。
有关关联获取的更多信息,请参阅 Hibernate ORM 文档。
1.5.4. 启用二级缓存
减少数据库访问次数的一种经典方法是使用二级缓存,允许在会话之间共享缓存数据。
Hibernate Reactive 支持不执行阻塞 I/O 的二级缓存实现。
确保您禁用首选缓存实现使用的任何基于磁盘的存储或分布式复制。使用阻塞 I/O 与网络或基于磁盘的存储交互的二级缓存至少会部分抵消反应式编程模型的优势。 |
配置 Hibernate 的二级缓存是一个相当复杂的话题,超出了本文档的范围。但是,如果它有所帮助,我们正在使用以下配置测试 Hibernate Reactive,该配置使用 EHCache 作为缓存实现,如上面的 可选依赖项 中所示。
配置属性名称 | 属性值 |
---|---|
|
|
|
|
|
|
|
|
如果您使用 EHCache,您还需要包含一个 ehcache.xml
文件,该文件显式配置属于您的实体和集合的每个缓存区域的行为。
不要忘记,您需要使用来自 org.hibernate.annotations 的 @Cache 注释显式标记将存储在二级缓存中的每个实体。 |
有关二级缓存的更多信息,请参阅 Hibernate ORM 文档。
1.5.5. 会话缓存管理
实体实例不会在不再需要时自动从会话缓存中逐出。(就这方面而言,会话缓存与二级缓存完全不同!)相反,它们会保留在内存中,直到它们所属的会话被您的程序丢弃。
detach()
和 clear()
方法允许您从会话缓存中删除实体,使其可用于垃圾回收。由于大多数会话都很短命,因此您不会经常需要这些操作。如果您发现自己认为在某种情况下需要它们,您应该认真考虑另一种解决方案:无状态会话。
1.5.6. 无状态会话
Hibernate 的一个鲜为人知的特性是 StatelessSession
接口,它提供了一种面向命令的、更基础的方法来与数据库交互。
您可以从 SessionFactory
获取反应式无状态会话
Stage.StatelessSession ss = getSessionFactory().openStatelessSession();
无状态会话
-
没有一级缓存(持久化上下文),也不与任何二级缓存交互,并且
-
不实现事务性写入后或自动脏检查,因此所有操作都在显式调用时立即执行。
对于无状态会话,您始终使用分离的对象。因此,编程模型略有不同
方法名称和参数 | 效果 |
---|---|
|
通过执行 |
|
获取分离对象的关联 |
|
通过执行 |
|
立即将给定瞬态对象的狀態 |
|
立即将给定分离对象的狀態 |
|
立即从数据库中 |
没有 flush() 操作,因此 update() 始终是显式的。 |
在某些情况下,这使得无状态会话更容易使用,但需要注意的是,无状态会话更容易受到数据别名影响,因为很容易获得两个不相同的 Java 对象,它们都代表数据库表的同一行。
如果您在无状态会话中使用 fetch() ,您可以非常轻松地获得两个表示同一数据库行的对象! |
特别是,没有持久化上下文意味着您可以安全地执行批量处理任务,而不会分配大量的内存。使用 StatelessSession
消除了调用以下操作的需要
-
clear()
或detach()
以执行一级缓存管理,以及 -
setCacheMode()
以绕过与二级缓存的交互。
无状态会话很有用,但是对于大型数据集的批量操作,Hibernate 不可能与存储过程竞争! |
使用无状态会话时,您应该注意以下其他限制
-
持久性操作永远不会级联到关联的实例,
-
无法对
@ManyToMany
关联和@ElementCollection
进行持久性更改,以及 -
通过无状态会话执行的操作会绕过回调。
1.5.7. 乐观锁和悲观锁
最后,我们在上面没有提到的行为负载方面是行级数据争用。当许多事务尝试读取和更新相同数据时,程序可能会由于锁升级、死锁和锁获取超时错误而变得无响应。
Hibernate 中有两种基本的数据并发方法
-
使用
@Version
列的乐观锁,以及 -
使用 SQL
for update
语法(或等效语法)的数据库级悲观锁。
在 Hibernate 社区中,使用乐观锁非常普遍,Hibernate 使其变得极其容易。
在多用户系统中,尽可能避免在用户交互期间保持悲观锁。事实上,通常的做法是避免跨用户交互执行事务。对于多用户系统,乐观锁是王者。 |
也就是说,悲观锁也确实有其用途,它有时可以降低事务回滚的概率。
因此,反应式会话的 find()
、lock()
和 refresh()
方法接受可选的 LockMode
。您也可以为查询指定 LockMode
。锁模式可用于请求悲观锁,或自定义乐观锁的行为
LockMode 类型 |
含义 |
---|---|
|
在使用 |
|
从数据库读取实体时获取的乐观锁,并在事务完成后使用 |
|
从数据库读取实体时获取的乐观锁,并在事务完成后使用 |
|
在使用 |
|
悲观 |
|
悲观 |
|
使用立即 |
1.6. 自定义连接管理和多租户
Hibernate Reactive 支持通过让您定义自己的 ReactiveConnectionPool
实现或扩展内置实现 DefaultSqlClientPool
来自定义反应式连接的管理。
配置属性名称 | 值 |
---|---|
|
实现 |
定义自定义池的常见动机是需要支持多租户。在多租户应用程序中,数据库或数据库模式取决于当前租户标识符。在 Hibernate Reactive 中设置此操作的最简单方法是扩展 DefaultSqlClientPool
并覆盖 getTenantPool(String tenantId)
。
对于多租户,可能还需要设置由 Hibernate ORM 定义的以下配置属性
配置属性名称 | 值 |
---|---|
|
(可选)实现 |
如果您没有提供 CurrentTenantIdentifierResolver
,则可以在调用 openSession()
、withSession()
或 withTransaction()
时显式指定租户 id。
如果您使用鉴别器 驱动的多租户,则不需要自定义池。在这种情况下,您只需要声明实体的 @TenantId 属性,就像在 Hibernate ORM 6 中一样。 |