Ohhnews

分类导航

$ cd ..
Jetbrains Blog原文

Hibernate 7.4 新特性

#hibernate#orm#分页查询#数据审计#历史数据

Hibernate 7.4 引入了 多项改进,简化了分页加载数据及其关联子集合、历史数据访问和审计日志记录。

本文将重点介绍以下特性:

  • 分页与 Fetch 连接:Hibernate 7.4 如何改进包含关联抓取的分页查询。
  • 历史表与审计表:新功能如何支持跨时间查询实体状态以及处理历史数据。

本文的示例代码可在此 GitHub 仓库 中找到。

分页与 Fetch 连接

在数据驱动的应用中,一个常见需求是加载一页父实体及其关联的子实体集合。例如,假设应用有一个 Order 实体,包含 Set<OrderItem> 集合,我们希望加载前几笔订单及其订单项。

$ java
List<Order> orders = session
        .createSelectionQuery(
            "select o from Order o join fetch o.items order by o.id",
            Order.class
        )
        .setMaxResults(10)
        .getResultList();

在 Hibernate 7.4 之前的版本中,对使用了集合 fetch 连接的查询应用分页,无法安全地下推到数据库层。由于每个 Order 可能有多条 OrderItem 行,直接限制 SQL 结果可能会截断某个订单的项集合。为了避免返回不完整的集合,Hibernate 会从数据库加载所有匹配的行,然后在应用层内存中进行分页。

这种做法的结果是正确的,但可能非常昂贵。一个原本只加载 10 条订单的查询,如果表中有大量订单和订单项,实际仍可能读取大量行。

在 Hibernate 7.4 之前,生成的 SQL 如下:

$ query
select
    o1_0.id, i1_0.order_id, i1_0.id, i1_0.product_code,
    i1_0.quantity, o1_0.order_number, o1_0.status
from
    orders o1_0
        join
    order_items i1_0
    on o1_0.id=i1_0.order_id

如您所见,分页并未在 SQL 查询级别应用。因此它将加载所有 orders 及其关联的 order_items,这可能是一个非常昂贵的操作,甚至可能导致内存溢出。

您可以看到 Hibernate 记录的警告如下:

[WARN] HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

防止 Hibernate 在内存中执行分页的一种方法是设置以下属性:

$ properties
hibernate.query.fail_on_pagination_over_collection_fetch=true

通过配置此属性,Hibernate 会抛出异常,而不是在内存中执行分页。

Hibernate 7.4 通过使用嵌套查询解决了这个问题。它不再将分页直接应用于连接结果集,而是首先确定有限的父实体标识符集,然后仅为这些父行获取关联集合。

这使得分页能够在数据库中完成,同时为每个选中的 Order 返回完整的项集合。

使用 Hibernate 7.4,SQL 将生成如下:

$ query
select
        o1_0.id, i1_0.order_id, i1_0.id, i1_0.product_code,
        i1_0.quantity, o1_0.order_number,o1_0.status 
    from
        (select
            o1_0.id, o1_0.order_number, o1_0.status 
        from
            orders o1_0 
        where
            exists(select
                1 from order_items i1_0 
            where
                o1_0.id=i1_0.order_id) 
        offset
            ? rows 
        fetch
            first ? rows only) o1_0(id, order_number, status) 
    join
        order_items i1_0 
            on o1_0.id=i1_0.order_id

这一改进使得 fetch 连接在分页场景下更加实用,例如订单列表页面显示每个订单及其明细行,而无需先加载完整结果集。

历史表与审计表

Hibernate 7.4 增加了对时态历史表和审计表的内置支持。这两个功能都有助于跟踪实体数据的变更,但服务于略有不同的用例:历史表允许我们查询实体在某个时间点的状态,而审计表记录实体发生的变更序列。

考虑以下 Product 实体:

$ java
@Entity
@Table(name = "products")
class Product {
    //字段 id, code, name, price
}

历史表

要为 Product 启用时态历史,使用 @Temporal 注解实体,并可选地使用 @Temporal.HistoryTable 指定历史表名称。

$ java
@Entity
@Table(name = "products")
@Temporal
@Temporal.HistoryTable(name="products_history")
class Product {
    //字段 id, code, name, price
}

通过此映射,Hibernate 将产品行的先前版本存储在 products_history 表中。该表包含实体列以及两个时态列:effective(标记版本何时生效)和 superseded(标记该版本何时被替换)。

products_history 表:

idcodenamepriceeffectivesuperseded
2251P1000Product-100040.002026-05-15 08:21:39.949001 +00:00null
2301P1001Product-100190.002026-05-15 08:22:24.765883 +00:002026-05-15 08:22:24.778067 +00:00
2301P1001Product-1001100.002026-05-15 08:22:24.778067 +00:00null

我们可以通过以下方式获取某个时间点的 Product 实体数据:

$ java
Instant someTime = ...
try (var session = sessionFactory.withOptions().asOf(someTime).open()) {
    var product = session.find(Product.class, productId);
}

这使得时态查询像普通实体查找一样自然,而 Hibernate 在后台解析正确历史行。

Hibernate 提供了多种不同策略(NATIVE、SINGLE_TABLE、HISTORY_TABLE)来映射时态实体。更多信息请参阅 时态数据 部分。

审计表

以前,基于 Hibernate 的应用程序通常使用独立的 Hibernate Envers 库来审计实体变更。Hibernate 7.4 将审计表支持直接集成到 Hibernate ORM 中,因此应用程序可以原生使用审计功能,而无需为此添加 Envers

通过添加 @Audited 可以启用审计,并可以使用 @Audited.Table 映射到自定义表。

$ java
@Entity
@Table(name = "products")
@Audited
@Audited.Table(name="products_aud_log")
class Product {
    //字段 id, code, name, price
}

启用审计后,Hibernate 每次变更会在审计表中写入一行。与历史表不同,审计表侧重于记录发生了什么操作以及何时发生。

idcodenamepricerevrevtype
2001P1002Product-100290.002026-05-13 14:58:17.505775 +00:000
2001P1002Product-1002100.002026-05-13 14:58:17.518194 +00:001

rev 值是变更发生的时间戳。revtype 值使用 ModificationType 枚举表示,如下:

$ java
public enum ModificationType {
    /**
    * 创建,编码为 0
    */
    ADD,
    /**
    * 修改,编码为 1
    */
    MOD,
    /**
    * 删除,编码为 2
    */
    DEL
}

更多信息请参阅 审计日志 部分。

总结

大多数应用程序使用分页来展示资源列表,我们过去需要编写自定义逻辑来加载分页数据及其关联子集合。现在这一功能已在框架层面得到处理。此外,我们过去依赖外部库(如 Envers)来实现审计,现在 Hibernate 自身已提供支持。

Hibernate 7.4 带来了实用的改进,解决了基于 JPA/Hibernate 应用程序中的实际问题。无论是优化分页查询行为还是跟踪历史数据,Hibernate 7.4 都减少了所需的自定义基础设施,并提供了更好的开箱即用支持,无需额外依赖库。

请使用此 GitHub 仓库 进一步探索这些新功能。