Hibernate Reactive 中文操作指南

1. Introduction to Hibernate Reactive

使用 Hibernate Reactive 创建一个新项目一点也不难。在本文档中,我们将介绍其中涉及的所有基本工作:

  1. 设置并配置项目,然后

  2. 编写 Java 代码来定义数据模型并访问数据库。

最后,我们将讨论一些与性能相关的话题,当您使用 Hibernate 开发任何大型项目时,您需要了解这些话题。

但是,在开始之前,我们建议您快速浏览 session-example 目录中非常简单的示例程序,其中展示了启动并运行您自己的程序所需的所有“部分”。

1.1. Information about Hibernate ORM

本文档假设对 Hibernate ORM 或 JPA 的另一种实现有一些了解。如果您以前从未使用过 JPA,这没关系,但您可能需要在本文中的某些要点中参考以下信息来源:

  1. documentation for Hibernate ORM

  2. JPA 2.2 specification,或

  3. Java Persistence with Hibernate,原名为 Hibernate in Action 书的最新版本。

1.2. Setting up a reactive Hibernate project

如果您在 Quarkus 环境之外使用 Hibernate Reactive,您需要:

  1. 将 Hibernate Response 本身连同适当的 Vert.x reactive 数据库客户端作为项目的依赖项包含,然后

  2. 使用 Hibernate 配置属性,配置有关数据库的信息的 Hibernate Reactive。

或者,如果您想在 Quarkus 中使用 Hibernate Reactive,您可以生成预配置的框架项目 right here

1.2.1. Including Hibernate Reactive in your project build

向您的项目添加以下依赖项:

其中 {version} 是您使用的 Hibernate Reactive 的版本。

您还需要为数据库添加 Vert.x 反应式数据库驱动程序的依赖项,以下选项之一:

Database

Driver dependency

PostgreSQL or CockroachDB

io.vertx:vertx-pg-client:{vertxSqlClientVersion}

MySQL or MariaDB

io.vertx:vertx-mysql-client:{vertxSqlClientVersion}

DB2

io.vertx:vertx-db2-client:{vertxSqlClientVersion}

SQL Server

io.vertx:vertx-mssql-client:${vertxSqlClientVersion}

Oracle

io.vertx:vertx-oracle-client:${vertxSqlClientVersion}

其中 {vertxSqlClientVersion} 是与您正在使用的 Hibernate Reactive 版本兼容的 Vert.x 版本。

您不需要依赖于数据库的 JDBC 驱动程序。

1.2.2. Optional dependencies

您还可以选择添加以下任何附加功能:

Optional feature

Dependencies

An SLF4J logging implementation

org.apache.logging.log4j:log4j-core or org.slf4j:slf4j-jdk14

Hibernate 元模型生成器(如果你正在使用 JPA 标准查询 API)

org.hibernate.orm:hibernate-jpamodelgen

Hibernate Validator

org.hibernate.validator:hibernate-validator and org.glassfish:jakarta.el

对 HQL 查询的编译时检查

org.hibernate:query-validator

通过 JCache 和 EHCache 提供二级缓存支持

org.hibernate.orm:hibernate-jcache along with org.ehcache:ehcache

PostgreSQL 的 SCRAM 身份验证支持

com.ongres.scram:client:2.1

如果您想使用字段级别的延迟提取,还可以将 Hibernate bytecode enhancer 添加到您的 Gradle 构建中。

示例程序中包含一个示例 Gradle build

1.2.3. Basic configuration

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>

documentation for Hibernate ORM 中可能找到 Hibernate 识别的配置属性的完整列表。您永远不需要触及其中的大多数。您在此阶段需要的属性是这三个:

Configuration property name

Purpose

jakarta.persistence.jdbc.url

数据库的 JDBC URL

jakarta.persistence.jdbc.user and jakarta.persistence.jdbc.password

Your database credentials

这些配置属性的名称中都有 jdbc,但当然 Hibernate Reactive 中没有 JDBC,这些只是 JPA 规范定义的遗留属性名称。特别是 Hibernate Reactive 本身解析并解释 JDBC URL。

Vert.x 数据库客户端已内置连接池和准备好的语句缓存。您可能希望控制连接池的大小:

Configuration property name

Purpose

hibernate.connection.pool_size

反应连接池的最大大小

我们将在以后的 @“78”中了解更高级的连接池调优。

1.2.4. Automatic schema export

您可以让 Hibernate Reactive 从您在 Java 代码中指定的映射注释中推断出您的数据库架构,并通过指定以下一个或多个配置属性在初始化时导出架构:

Configuration property name

Purpose

jakarta.persistence.schema-generation.database.action

如果 create,请先删除架构,然后导出表格、序列和约束。如果 create-only,则导出表格、序列和约束。如果 create-drop,请删除架构并在 SessionFactory 启动时重新创建它。另外,在 SessionFactory 关闭时删除架构。如果 drop,请在 SessionFactory 关闭时删除架构。如果 validate,请验证数据库架构,但不进行更改。如果 update,请仅导出架构中缺少的内容。

jakarta.persistence.create-database-schemas

(可选的)如果 true,自动创建架构和目录

jakarta.persistence.schema-generation.create-source

(可选的)如果 metadata-then-scriptscript-then-metadata,在导出的表格和序列时执行附加的 SQL 脚本

jakarta.persistence.schema-generation.create-script-source

(可选)要执行的 SQL 脚本的名称

此特性对于测试极有帮助。

模式导出使用阻塞操作,因此使用时启动工厂可能需要特殊处理。如果不这样做,将会引发异常:

io.vertx.core.VertxException: Thread blocked

以下方法可解决该问题:

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. Logging the generated SQL

若要查看发送到数据库的已生成 SQL,请执行以下操作:

  1. 将属性 hibernate.show_sql 设置为 true,或

  2. 使用首选的 SLF4J 日志记录实现,启用类别 org.hibernate.SQL 的调试级别日志记录。

例如,如果您使用的是 Log4j 2(如上面 link:#@“79”文件中的):

logger.hibernate.name = org.hibernate.SQL
logger.hibernate.level = debug

一个示例 log4j2.properties 文件包含在示例程序中。

通过启用以下一个或两个设置,你可以使已记录的 SQL 更具可读性:

Configuration property name

Purpose

hibernate.format_sql

如果指定 true,将以多行缩进格式记录 SQL

hibernate.highlight_sql

如果指定 true,将通过 ANSI 转义代码将 SQL 与语法高亮一起记录

1.2.6. Minimizing repetitive mapping information

以下属性对于最小化您需要在 @“80”和 @“81”注释中显式指定的信息量非常有用,我们将在下面的 @“82”中讨论这些注释:

Configuration property name

Purpose

hibernate.default_schema

没有明确声明的实体的默认模式名称

hibernate.default_catalog

没有明确声明的实体的默认目录名称

hibernate.physical_naming_strategy

一个 PhysicalNamingStrategy,用于实施数据库命名标准

1.2.7. Nationalized character data in SQL Server

By default, SQL Server 的 charvarchar 类型不支持 Unicode 数据。因此,如果你使用 SQL Server,你可能需要强制 Hibernate 使用 ncharnvarchar 类型。

Configuration property name

Purpose

hibernate.use_nationalized_character_data

使用 ncharnvarchar,而不是 charvarchar

1.3. Writing the Java code

至此,我们已准备好编写 Java 代码!

与使用 Hibernate 的任何项目类似,你的持久化相关代码包含两个主要部分:

  • 以一组带注释的实体类的形式表示数据模型,以及

  • 大量与 Hibernate 的 API 交互的函数,以执行与各种事务关联的持久性操作。

第一部分,即数据或“域”模型,通常更容易编写,但是出色地、干净利落地完成这项工作会极大地影响您在第二部分的成功。

代码的第二部分更难正确编写。此代码必须:

  1. 管理事务和 reactive 会话,

  2. 通过链接在 reactive 会话中调用的持久化操作来构造 reactive 流,

  3. 获取并准备用户界面需要的数据,以及

  4. handle failures.

1.3.1. Mapping entity classes

我们在此不会过多介绍实体类,仅仅是因为在 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...
}

你可以自由组合和匹配:

  1. jakarta.persistence 包中定义的常规 JPA 映射注释,带有

  2. org.hibernate.annotations 中的高级映射注释,甚至

  3. 由 Bean Validation 定义的像 @NotNull@Size 这样的注释。

documentation for Hibernate ORM 中可能找到对象/关系映射注释的完整列表。大多数映射注释在 Hibernate Reactive 中已经得到支持,不过此时仍有一些限制。

Common JPA annotations

最常用且最有用的映射注解包括这些标准 JPA 注解:

Annotation

Purpose

@Entity

声明实体类(具有自己的数据库表和永久性标识符的类)

@MappedSuperclass

声明其 @Entity 子类的通用持久化字段的超类

@Embeddable or @Embedded

声明可嵌入类(没有自己的持久化标识符或数据库表的类)

@Inheritance

定义如何将继承层次映射到数据库表

@Id

指定实体的字段保存实体的持久性标识符,并映射到其表格的主键

@IdClass

指定一个类,表示实体的复合主键(对于具有多个 @Id 字段的实体)

@EmbeddedId

指定实体的字段保存作为 @Embeddable 类表示的复合主键

@GeneratedValue

指定标识符是系统生成的替代密钥

@Version

指定实体的字段保留用于乐观锁定的版本号

@Enumerated

将包含 enum 的字段映射起来

@ManyToOne

声明一个多对一关联到第二个实体

@OneToOne

声明一个一对一关联到第二个实体

@OneToMany

声明一个一对多关联到第二个实体

@Table

指定映射到一个数据库表

@SecondaryTable

指定映射到第二个数据库表

@Column

指定映射到一个数据库列

@JoinColumn

指定映射到一个数据库外键

Useful Hibernate annotations

了解这些 Hibernate 注解也非常有用:

Annotation

Purpose

@Cache

针对一个实体启用二级缓存

@Formula

映射字段,而不是列,到 SQL 表达式

@CreationTimestamp, @UpdateTimestamp

自动给一个字段分配时间戳

@OptimisticLocking

对没有 @Version 字段的实体启用乐观锁定

@FilterDef and @Filter

Define a Hibernate filter

@FetchProfile

定义一个 Hibernate 抓取配置文件

@Generated

由数据库生成的一个属性

@ColumnDefault

指定用于为一个列分配默认值的 SQL 表达式(与 @Generated(INSERT) 组合使用)

@GenericGenerator

选择一个自定义 ID 生成器

@DynamicInsert and @DynamicUpdate

只用必需的列动态生成 SQL(而不是使用在启动时生成的静态 SQL)

@Fetch

指定一个关联的获取模式

@BatchSize

指定一个关联的批量获取的批量大小

@Loader

指定一个用于通过 id 获取一个实体的命名查询(例如,当调用 find(type, id) 时),来替代 Hibernate 生成的默认 SQL

@SqlInsert, @SqlUpdate, @SqlDelete

为实体操作指定自定义 DML

@NaturalId

将一个或多个字段标记为实体的备用“自然”标识符(唯一键)

@Nationalized

有选择地对一特定列使用 ncharnvarchar 或者 nclob

@Immutable

指定一个实体或者集合是不可变的

@SortNatural or @SortComparator

映射一个 SortedSet 或者 SortedMap

@Check

声明要添加到 DDL 中的 SQL check 约束

Bean Validation annotations

有关 Bean Validation 注释的信息可以在 documentation for Hibernate Validator 中找到。

1.3.2. Getters and setters

在 Hibernate Reactive outside Quarkus 环境中使用时,你需要根据通常的 JPA 约定编写实体类,其中要求:

  1. 持久属性的私有字段,以及

  2. a nullary constructor.

禁止从实体类外部访问持久字段。因此,必须通过实体类定义的 getter 和 setter 方法来中介对持久字段的外部访问。

在 Quarkus 中使用 Hibernate Reactive 时,这些要求已放松,如果您愿意,可以使用公共字段代替 getter 和 setter。

1.3.3. equals() and hashCode()

实体类应覆盖 equals()hashCode()。对于初次接触 Hibernate 或 JPA 的人来说,经常会对应该在 hashCode() 中包含哪些字段感到困惑,请记住以下原则:

  1. 不要在哈希代码中包含可变字段,因为每当字段发生突变时,这将需要重新散列包含该实体的任何集合。

  2. It’s not completely wrong to include a generated identifier (surrogate key) in the hashcode, but since the identifier is not generated until the entity instance is made persistent, you must take great care to not add it to any hashed collection before the identifier is generated. We therefore advise against including any database-generated field in the hashcode.

将任何不可变的非生成字段包含在哈希中是没有问题的。

也就是说,基于实体的生成标识符实现 equals()hashCode() 也可用作 if you’re careful

请注意,即使您已标识出自然键,我们仍建议在外部键中使用生成的代理键,因为这可以使您的数据模型 much 更易于更改。

1.3.4. Identifier generation

Hibernate Reactive 的功能与纯 Hibernate 有所不同的一个领域是 ID 生成领域。为与 Hibernate ORM 和 JDBC 配合使用而编写的自定义标识符生成器无法在反应式环境中使用。

  1. 序列、表和 UUID id 生成已内置,并且可以使用常规 JPA 映射注释 (@GeneratedValue@TableGenerator@SequenceGenerator) 选择这些 id 生成策略。

  2. 在 MySQL 上,可以通过指定 @GeneratedValue(strategy=GenerationType.IDENTITY) 来使用自动增量列

  3. 可以通过实现 ReactiveIdentifierGenerator 并使用 @GenericGenerator 声明自定义实现来定义自定义 id 生成器。

  4. 程序中的自然 id(包括组合 id)可以用通常的方式分配。

JPA 规范定义的标准 ID 生成策略可以使用以下注解进行定制:

Annotation

Purpose

@SequenceGenerator

根据数据库序列配置生成器

@TableGenerator

根据数据库表的行配置生成器

例如,序列 ID 生成可以这样指定:

@Entity
@Table(name="authors")
class Author {
    @Id @GeneratedValue(generator = "authorIds")
    @SequenceGenerator(name = "authorIds",
               sequenceName = "author_ids",
             allocationSize = 20)
    Integer id;
    ...
}

您可以在 JPA 规范中找到更多信息。

如果您有非常特殊的要求,那么您可以查看 ReactiveIdentifierGenerator 的 Javadoc,以了解如何实现您自己的自定义 reactive 标志符生成器。

1.3.5. Custom types

基于 UserType 接口的 Hibernate 自定义类型针对 JDBC 使用,并依赖于 JDBC 定义的接口。因此,Hibernate Reactive 提供了一个适配器,向 UserType 实现公开了 JDBC 部分实现。

因此,some 现有的 UserType 实现可以在 Hibernate Reactive 中使用,具体取决于它们依赖于 JDBC 的哪些特性。

您可以通过使用 Hibernate @Type 注解来注释实体类的字段,以指定自定义类型。

1.3.6. Attribute converters

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

您需要使用以下注解中的一种或两种:

Annotation

Purpose

@Converter

声明一个实现 AttributeConverter 的类

@Convert

指定一个 AttributeConverter 转换器以用于实体类的字段

您可以在这些注解的 Javadoc 和 JPA 规范中找到更多信息。

1.3.7. APIs for chaining reactive operations

当您使用 Hibernate Reactive 编写持久性逻辑时,您大部分时间都在使用 reactive Session。只是为了让新用户有点无所适从,reactive Session 及其相关的接口都有两个版本:

  1. Stage.Session 及其朋友提供了一个基于 Java 的 CompletionStage 的 reactive API,以及

  2. Mutiny.Session 及其朋友提供了一个基于 Mutiny 的 API。

您需要决定要使用哪个 API!

以下是在使用 Hibernate Reactive 时始终需要的反应流中最重要的操作:

Purpose

Java CompletionStage

Mutiny Uni

Chain non-blocking operations

thenCompose()

chain()

Transform streamed items

thenApply()

map() and replaceWith()

使用流式数据执行操作

thenAccept()

invoke() and call()

执行清理(类似于 finally

whenComplete()

eventually()

在本简介中,我们的代码示例通常使用 Mutiny。如果您更熟悉 CompletionStage,可以参考上表帮助理解代码。

当我们在本文档中使用术语 reactive stream 时,我们的意思是:

  1. 一个 CompletionStage 的链,或

  2. 一个 Mutiny UniMulti 的链

由程序构建,以服务于特定请求、事务或工作单元。

1.3.8. Obtaining a reactive session factory

无论您决定什么,获得 reactive 会话的第一步是获取 JPA EntityManagerFactory,就像您通常在普通 JPA 中获得 JPA EntityManagerFactory 一样,例如,通过调用:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("example");

现在,unwrap() reactive SessionFactory。如果您想使用 CompletionStage_s for chaining reactive operations, ask for a _Stage.SessionFactory

Stage.SessionFactory sessionFactory = emf.unwrap(Stage.SessionFactory.class);

或者,如果您更愿意使用基于 Mutiny 的 API,unwrap() 类型 Mutiny.SessionFactory

Mutiny.SessionFactory sessionFactory = emf.unwrap(Mutiny.SessionFactory.class);

reactive 会话可以从结果 reactive SessionFactory 中获得。

1.3.9. Obtaining a reactive session

持久性操作通过 reactive Session 对象公开。理解此接口的大多数操作都是非阻塞的非常重要,并且针对数据库执行 SQL 从不会同步进行。属于单个工作单元的持久性操作必须在单个 reactive 流中通过组合进行链接。

此外请记住,Hibernate 会话是一个轻量级对象,应该在单个工作逻辑单元内创建、使用,然后丢弃。

若要从 SessionFactory 中获取响应 Session,请使用 withSession()

sessionFactory.withSession(
        session -> session.find(Book.class, id)
                .invoke(
                    book -> ... //do something with the book
                )
);

生成的对象在自动与当前的响应流相关联,因此在给定流中嵌套调用 in 给定流中嵌套调用 automatically 自动获得相同的共享会话。

或者,您也可以使用 @“83”,但您必须记住在完成后“关闭”会话。而且您必须非常小心地仅从一个 Vert.x 上下文中访问每个会话。(请参阅 @“85”了解更多信息)。

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. Using the reactive session

接口具有与 JPA 方法同名的多个方法。您可能已熟悉 JPA 定义的以下会话操作:

Method name and parameters

Effect

find(Class,Object)

根据类型和 ID(主键)获取持久化对象

persist(Object)

使瞬态对象持久化,并计划一个 SQL insert 语句供稍后执行

remove(Object)

使持久化对象瞬态,并计划一个 SQL delete 语句供稍后执行

merge(Object)

将给定分离对象的 state 复制到一个对应的受管持久化实例,并返回持久化对象

refresh(Object)

使用新的 SQL select 来刷新对象的持久化状态,从数据库中检索当前状态

lock(Object)

对持久化对象获取悲观锁

flush()

检测对 session 关联的持久化对象所做的更改,并通过执行 SQL insertupdatedelete 语句使数据库 state 与 session 的 state 同步

detach(Object)

在不影响数据库的情况下使持久化对象与 session 解关联

getReference(Class,id) or getReference(Object)

获取持久化对象的引用,而不从数据库中实际加载其状态

如果您不熟悉这些操作,请不要绝望!它们的语义在 JPA 规范和 API 文档中进行定义,并且在无数篇文章和博客文章中进行了说明。但如果您已有一些 Hibernate 或 JPA 经验,那么您已经驾轻就熟了!

现在,here’s where Hibernate Reactive is different: 在响应 API 中,这些方法中的每一个都会通过 Java CompletionStage(或 Mutiny Uni)以非阻塞方式返回其结果。例如:

session.find(Book.class, book.id)
       .invoke( book -> System.out.println(book.title + " is a great book!") )

另一方面,没有有意义返回值的方法只需返回 (或 )。

session.find(Book.class, id)
       .call( book -> session.remove(book) )
       .call( () -> session.flush() )

使用响应流时,一个 extremely 常见的错误是忘记将“类似 void”的方法的返回值链接起来。例如,在以下代码中,flush() 操作从未执行,因为 invoke() 未将返回值链接到流的末端。

session.find(Book.class, id)
       .call( book -> session.remove(book) )
       .invoke( () -> session.flush() )   //OOPS, WRONG!!

因此,请记住:

  1. 调用返回 CompletionStage_的“类似于 void”的方法时,必须使用 _thenCompose(),而不是 thenAccept()

  2. 在 Mutiny 中,调用返回 Uni_的“类似于 void”的方法时,必须使用 _call(),而不是 invoke()

相同的问题发生在以下代码中,但这次是 永远不会被调用的:

session.find(Book.class, id)
       .call( book -> {
           session.remove(book);   //OOPS, WRONG!!
           return session.flush();
       } )

如果您已有一些响应式编程经验,那么这里没有新的知识点。但如果您 对响应式编程很感兴趣,只要知道以某种形式您将犯一次此类错误!

1.3.11. Queries

自然,接口是一个工厂,用于 实例,使您能够设置查询参数以及执行查询和 DML 语句:

Method name

Effect

createQuery()

获取 Query 来执行用 HQL 或 JPQL 编写的查询或 DML 语句

createNativeQuery()

获取 Query 来执行用数据库的本机 SQL 方言编写的查询或 DML 语句

createNamedQuery()

获取 Query 来执行由 @NamedQuery 注释定义的具名 HQL 或 SQL 查询

该方法生成一个响应式 ,允许异步执行 HQL/JPQL 查询,始终通过 (或 )返回其结果:

session.createQuery("select title from Book order by title desc")
       .getResultList()
       .invoke( list -> list.forEach(System.out::println) )

接口定义了以下重要操作:

Method name

Effect

setParameter()

设置查询参数的参数

setMaxResults()

限制查询返回的结果数

setFirstResult()

指定跳过一定数量的初始结果(用于结果翻页)

getSingleResult()

执行查询并获取单个结果

getResultList()

执行查询并获取结果列表

executeUpdate()

执行 DML 语句并获取受影响的行数

对于 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. Fetching lazy associations

在 Hibernate ORM 中,当关联首次在会话中访问时,将透明地获取延迟关联。而在 Hibernate 响应式中,延迟关联获取是一个异步进程,通过 (或 )产生结果。

因此,延迟获取是一个显式操作,称为 ,的静态方法 和 :

session.find(Author.class, author.id)
       .chain( author -> Mutiny.fetch(author.books) )
       .invoke( books -> ... )

当然,如果您急切地获取关联,这不是必需的。

有时你可能需要将多个调用链接到 fetch(),例如:

Mutiny.fetch( session.getReference(detachedAuthor) )
       .chain( author -> Mutiny.fetch(author.books) )
       .invoke( books -> ... )

1.3.13. Field-level lazy fetching

同样,字段级延迟获取(一项高级功能,仅在与 Hibernate 的可选的编译时字节码增强器结合使用时受支持)也是一项显式操作。

为声明延迟字段,我们通常使用 JPA 注解:

@Basic(fetch=LAZY) String isbn;

一个声明为 的可选的一对一关联也被视为字段级的延迟关联。

只有在通过调用 fetch() 操作的重载版本显式请求时,才获取延迟字段:

session.find(Book.class, book.id)
       .chain( book -> session.fetch(book, Book_.isbn) )
       .invoke( isbn -> ... )

请注意,要获取的字段由 JPA 元模型 标识。

1.3.14. Transactions

withTransaction() 方法在数据库事务范围内执行工作。

session.withTransaction( tx -> session.persist(book) )

会话在事务结束后自动刷新。

对于给定的 Session 对象,对 withTransaction() 的嵌套调用在相同的共享事务上下文中发生。但是,请注意,事务仅为 resource local 事务,委派给底层 Vert.x 数据库客户端,不会跨越多个数据源,也不会与 JPA 容器管理事务集成。

为了提供额外的便利,有一种打开会话并在一次调用中启动事务的方法:

sessionFactory.withTransaction( (session, tx) -> session.persist(book) )

这可能是大多数时间最方便使用的方法。

1.4. Integrating with Vert.x

在运行时,与数据库的交互在 Vert.x 线程中进行,通常是事件循环线程中。当您编写创建和销毁 Hibernate Reactive 会话的代码时,了解会话如何与线程和 Vert.x contexts 相关非常重要。

1.4.1. Sessions and Vert.x contexts

当你使用 withSession()withTransaction() 创建会话时,它会自动与当前 Vert.x local context 关联,并随本地上下文传播,如上文 Obtaining a reactive session 中所述。而且你只能从拥有此本地上下文的线程中使用会话。如果你搞砸了,并从其他线程中使用它,你可能会看到此错误:

另一方面,如果使用 openSession(),则必须自己管理会话和上下文之间的关联。现在,这在原则上很简单,但您会惊讶于人们经常出错。

不少用户对这个限制感到惊讶。但我们坚持认为这是完全自然的。从你作为会话用户角度来看,会话的一个原子操作就像 flush()find()_或 _getResultList() 等方法。其中任何一种方法都可能导致 multiple interactions with the database。在这种交互之间,会话根本不是一个定义良好的状态。反应流是一种线程,期望反应式编程应该用一阵耀眼的精灵粉尘消除你的并发问题是不合理的。事情不是这样运作的。

例如,我敢打赌您想编写类似的代码:

List<CompletionStage> list = ...
for (Entity entity : entities) {
    list.add(session.persist(entity));
}
CompletableFuture.allOf(list).thenCompose(session::flush);

抱歉,但这不被允许。并行反应流可能不会共享会话。每个流必须有自己的会话。

1.4.2. Executing code in a Vert.x context

如果您需要在 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 instance service

通过 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. Tuning and performance

一旦您有一个使用 Hibernate Reactive 访问数据库的程序启动并运行,必然会发现一些性能令人失望或无法接受的地方。

幸运的是,只要记住一些简单的原则,大多数性能问题都可以通过 Hibernate 为你提供的工具轻松解决。

首先也是最重要的:您使用 Hibernate Reactive 的原因是因为它让事情变得更简单。如果对于某个问题,它会让事情变得 harder,请停止使用它。相反,使用不同的工具解决此问题。

第二:在使用 Hibernate 的程序中,有两个主要的潜在性能瓶颈来源:

  1. 往返数据库的次数过多,且

  2. 与一级(会话)缓存关联的内存消耗。

因此,性能调优主要涉及减少对数据库的访问次数,和/或控制会话缓存的大小。

但在我们讨论这些更高级的话题之前,我们应该先调整连接池。

1.5.1. Tuning the Vert.x pool

在 @“86”中,我们已经看到如何设置 Vert.x 数据库连接池的大小。当需要进行性能调优时,可以通过以下配置属性进一步自定义池和已准备的语句缓存:

Configuration property name

Purpose

hibernate.vertx.pool.max_wait_queue_size

等待队列中允许的最大连接请求数

hibernate.vertx.pool.connect_timeout

以毫秒为单位,请求连接池连接时的最大等待时间

hibernate.vertx.pool.idle_timeout

连接处于空闲状态的最长时间(以毫秒为单位)

hibernate.vertx.pool.cleaner_period

Vert.x 连接池清除时间(以毫秒为单位)

hibernate.vertx.prepared_statement_cache.max_size

预处理语句缓存的最大大小

hibernate.vertx.prepared_statement_cache.sql_limit

要缓存的预处理语句 SQL 字符串的最大长度

最后,对于更高级的案例,您可以编写自己的代码来通过实现 SqlClientPoolConfiguration 配置 Vert.x 客户端。

Configuration property name

Purpose

hibernate.vertx.pool.configuration_class

A class implementing SqlClientPoolConfiguration

1.5.2. Enabling statement batching

提高某些事务性能且几乎不需要任何工作的一种简单方法是启用自动 DML 语句批处理。只有在程序在一个事务中对同一张表执行许多插入、更新或删除操作的情况下,批处理才能提供帮助。

您需要做的所有事情是设置一个属性:

Configuration property name

Purpose

hibernate.jdbc.batch_size

SQL 语句批处理的最大批处理大小

(同样,该属性名称中包含 jdbc,但 Hibernate Reactive 重新设置其用于响应连接。)

1.5.3. Association fetching

在 ORM 中实现高性能意味着尽量减少与数据库的往返次数。无论在何时使用 Hibernate 编写数据访问代码时,这个目标都应该放在首位。ORM 中最基本的经验法则就是:

  1. 在一个会话/事务的开始就明确指定需要的所有数据,并立即在一次或两次查询中获取,

  2. 然后才开始在持久实体之间导航关联。

毫无疑问,Java 程序中执行不佳的数据访问代码最常见的原因是 N+1 selects 的问题。此处,在初始查询中从数据库中检索了一行 N 行,然后使用 N 个后续查询来获取相关实体的关联实例。

Hibernate 提供了多种策略用于高效获取关联并避免 N+1 选择:

  1. outer join fetching,

  2. batch fetching, and

  3. subselect fetching.

其中,您几乎应始终使用外部连接获取。批获取和子选择仅在极少数情况下有用,其中外部连接获取将导致笛卡尔积和巨大的结果集。不幸的是,外部连接获取根本无法与惰性获取一起使用。

由此建议,你不应该太频繁地使用 Stage.fetch()Mutiny.fetch()

现在,我们并不是在说关联应该默认映射为急切获取!那将是一个可怕的想法,导致获取整个数据库的简单会话操作!因此:

听起来这个提示与前面的提示相矛盾,但事实并非如此。它表示你必须明确指定关联的快速提取,仅在需要的时候和需要的地方提取。

如果您在某些特定事务中需要急切获取,请使用:

  1. _left join fetch_在 HQL 中,

  2. a fetch profile,

  3. a JPA EntityGraph, or

  4. _fetch()_在条件查询中。

您可以在 documentation for Hibernate ORM 中找到有关关联获取的更多信息。

1.5.4. Enabling the second-level cache

减少访问数据库次数的经典方法是使用二级缓存,允许在会话之间共享缓存数据。

Hibernate Reactive 支持执行无阻塞 I/O 的二级缓存实现。

配置 Hibernate 的二级缓存是一个相当复杂的话题,并且超出了本文的范围。但如果它有帮助,我们正在使用以下配置测试 Hibernate Reactive,它使用 EHCache 作为上文 Optional dependencies 中的缓存实现:

Configuration property name

Property value

hibernate.cache.use_second_level_cache

true

hibernate.cache.region.factory_class

org.hibernate.cache.jcache.JCacheRegionFactory

hibernate.javax.cache.provider

org.ehcache.jsr107.EhcacheCachingProvider

hibernate.javax.cache.uri

/ehcache.xml

如果您正在使用 EHCache,您还需要包含一个 ehcache.xml 文件,该文件明确配置属于您的实体和集合的每个缓存区域的行为。

你可以在 documentation for Hibernate ORM 中找到有关二级缓存的更多信息。

1.5.5. Session cache management

不再需要实体实例时,它们不会自动从会话缓存中逐出。(在这方面,会话缓存与二级缓存完全不同!)相反,它们会一直保留在内存中,直到它们所属的会话被您的程序丢弃。

方法 detach()clear() 允许你从会话缓存中删除实体,使其可供垃圾回收。由于大多数会话的持续时间都很短,因此你不太需要进行这些操作。而且如果你发现自己在想自己在某种情况下需要 do,则应该认真考虑替代解决方案:一个 stateless session

1.5.6. Stateless sessions

Hibernate 的一个可能不受重视的功能是 StatelessSession 接口,它提供了一种面向命令、更接近底层的与数据库交互的方法。

您可从 SessionFactory 中获取无状态的响应式会话:

Stage.StatelessSession ss = getSessionFactory().openStatelessSession();

一个无状态会话:

  1. 没有一级缓存(持久性上下文),也不与任何二级缓存交互,且

  2. 不执行事务性后台写入或自动污点检查,因此所有操作在明确调用时都会立即执行。

对于无状态会话,您始终使用分离对象。因此,编程模型有点不同:

Method name and parameters

Effect

get(Class, Object)

通过执行 select 获取具有其类型和 id 的分离对象

fetch(Object)

获取分离对象的关联

refresh(Object)

通过执行 select 刷新分离对象的状态

insert(Object)

立即将给定瞬态对象的状态 insert 到数据库

update(Object)

立即将给定分离对象的状态 update 到数据库

delete(Object)

立即 delete 与数据库中的给定分离对象的状态分离

在某些情况下,这使得无状态会话更易于使用,但需要注意的是,无状态会话更容易受到数据别名效应的影响,因为很容易获得两个非标识的 Java 对象,它们都表示数据库表的同一行。

特别是,缺乏持久性上下文意味着你可以安全地执行批量处理任务,而无需分配大量的内存。使用 StatelessSession 无需调用:

  1. _clear()_或 _detach()_执行一级缓存管理,且

  2. _setCacheMode()_绕过与二级缓存的交互。

在使用无状态会话时,您应该了解以下额外的限制:

  1. 持久性操作永远不会级联到相关实例,

  2. @ManyToMany_关联和 _@ElementCollection 的更改无法持久化,并且

  3. 通过无状态会话执行的操作会绕过回调。

1.5.7. Optimistic and pessimistic locking

最后,我们没有在上面提到的受载期间行为的一个方面是行级别数据争用。当许多事务尝试读取和更新相同数据时,程序可能会因锁升级、死锁和锁获取超时错误而失去响应。

Hibernate 中有两种基本的数据并发方法:

  1. 使用 _@Version_列进行乐观锁定,并

  2. 使用 SQL _for update_语法(或等效语法)进行数据库级悲观锁定。

在 Hibernate 社区中,使用乐观锁定更常见,而 Hibernate 让这件事变得非常容易。

也就是说,那里 is 也有悲观锁,它有时可以降低事务回滚的可能性。

因此,响应式会话的 find()lock()、和 refresh() 方法接受一个可选的 LockMode。您还可以为查询指定 LockMode。锁定模式可用于请求悲观锁定,或自定义乐观锁定的行为:

LockMode type

Meaning

READ

当使用 select 从数据库中读取实体时,隐式获取乐观锁

OPTIMISTIC

当从数据库中读取实体时获取乐观锁,并在交易完成时使用 select 检查版本进行验证

OPTIMISTIC_FORCE_INCREMENT

当从数据库中读取实体时获取乐观锁,并在交易完成时使用 update 增量版本进行强制执行

WRITE

当使用 updateinsert 将实体写入数据库时,隐式获取悲观锁

PESSIMISTIC_READ

一个悲观 for share

PESSIMISTIC_WRITE

一个悲观 for update

PESSIMISTIC_FORCE_INCREMENT

使用立即 update 强制增量版本的悲观锁

1.6. Custom connection management and multitenancy

Hibernate Reactive 支持响应式连接的自定义管理,允许您定义 ReactiveConnectionPool 或者扩展内置实现 DefaultSqlClientPool 的自定义实现。

Configuration property name

Value

hibernate.vertx.pool.class

实现 ReactiveConnectionPool 的一个类

定义自定义池的常见动机是对多租户的支持需求。在多租户应用程序中,数据库或数据库模式依赖于当前租户标识符。在 Hibernate Reactive 中设置它的最简单方法是扩展 DefaultSqlClientPool 并覆盖 getTenantPool(String tenantId)

对于多租户,可能还需要设置由 Hibernate ORM 定义的以下配置属性:

Configuration property name

Value

hibernate.tenant_identifier_resolver

(可选)实现 CurrentTenantIdentifierResolver 的类

如果您不提供 CurrentTenantIdentifierResolver,您可以在调用 openSession()withSession() 或者 withTransaction() 时显式地指定租户 id。

1.7. Next steps

Hibernate Reactive 现在集成在 QuarkusPanache 中。Quarkus 中的配置略有不同,因此请务必查看 Quarkus 文档以了解详细信息。