Hibernate ORM 版本 6.6 已发布 Alpha 版本,最终版本将很快推出。在今天的文章中,我们将深入了解该版本的新特性之一,即新的 @ConcreteProxy 注解。

问题

Hibernate ORM 使用 实体代理 来启用 延迟关联检索,允许框架在真正需要时才检索关联实体的数据,即访问其属性时。实体代理也用于获取 实体引用,无需访问数据源来初始化它们的属性。

尽管实体代理在大多数情况下都透明地工作,作为 Hibernate 的用户,你不需要做任何特殊的事情,但有些情况下,它们的操作方式可能与普通实体实例不同。当一个关联是 多态 的,即它引用了一个具有子类型的实体类型时,代理并不了解它所代表的实体实例的具体子类型。子类型只有在代理被检索并且目标表已经被读取之后才知道。这显然在依赖于 Java 的 instanceof 运算符和类型转换时会导致问题。

考虑以下简单的实体映射

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "animal_type")
class Animal {
    @Id
    @GeneratedValue
    Long id;

    Long getId() {
        return id;
    }
}

class Cat extends Animal {
    String name;

    String getName() {
        return name;
    }
}

class Owner {
    @Id
    Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "animal_id")
    Animal animal;
}

当加载一个 Owner 实例时,其 animal 属性将是一个 代理 实例

Cat cat = new Cat();
cat.name = "Bella";

Owner owner = new Owner();
owner.id = 1L;
owner.animal = cat;

// later

Owner owner = session.find( Owner.class, 1L );

Hibernate.isInitialized( owner.animal ) // returns false

owner.animal instanceof Animal; // returns true
((Animal) owner.animal).getId(); // returns the id

owner.animal instanceof Cat; // returns false
((Cat) owner.animal).getName(); // throws ClassCastException

注意生成的 SQL 从未访问 Animal

select
    c1_0.id,
    c1_0.lazy_id
from
    Owner c1_0
where
    sp1_0.id=1

以前,解决这个问题唯一的方法是使用 Hibernate 类的静态实用方法,如 getClassLazy()unproxy(),但它们会在处理继承层次结构时导致早期初始化。

Hibernate 团队决定为我们提供一种与具有继承功能的实体类型一起工作的代理的替代方法,并保证它们将始终以适当的子类型创建。

在上下文中,Java语言一直在改进对基于instanceof逻辑的支持,包括Java 14的Pattern Matching for instanceof(见JEP 305)以及更近期的Java 17的Pattern Matching for switch(见JEP 406)。

解决方案

我们引入了新的@ConcreteProxy注解:当该注解放置在实体继承层次结构的根节点上时,这将告诉Hibernate在创建懒加载代理实例时始终解析实际实体类型。从之前的例子来看,这意味着

Owner owner = session.find( Owner.class, 1L );

owner.animal instanceof Cat; // returns true
Cat cat = (Cat) owner.animal;
Hibernate.isInitialized( cat ); // returns false, laziness is preserved
cat.getName(); // returns the Cat's name

懒加载的animal关联仍然包含一个未初始化的代理,但这次它尊重与已加载的Owner实例关联的实际子类型。这意味着懒加载将被保留,而任何instanceof检查或显式类型转换现在将按预期工作。

此功能并非免费:为了确定创建代理实例时要使用的具体类型,Hibernate可能需要访问实体的表来发现与特定标识符值对应的实际子类型。

这次之前的查询将包括与Animal表的left join,用于读取区分符值

select
    o1_0.id,
    o1_0.animal_id,
    a1_0.animal_type
from
    Owner o1_0
left join
    Animal a1_0
        on a1_0.id=o1_0.animal_id
where
    op1_0.id=1

具体类型将被确定

  • 单表继承中,当获取关联时,区分符列值被左连接,或者当获取引用时,直接从实体表中读取。

  • 当使用连接继承时,必须左连接所有子类型表以确定具体类型。请注意,然而,当使用显式区分符列时,行为与单表继承相同。

  • 最后,对于表按类继承,必须查询所有子类型表以确定具体类型。

以下是一个用于检索请求懒引用时Animal的具体类型的查询示例

select
    a1_0.animal_type
from
    Animal a1_0
where
    a1_0.id=1

有关更多信息和支持,您可以参考我们的Jira上的原始功能请求

接下来是什么

为了绕过每次创建代理时都需要通过left join访问懒加载关联的目标表的需求,Hibernate可以将区分符值直接存储在所有者端表上,包括外键本身。这种区分符值的反规范化将使@ConcreteProxy关联检索更有效,同时保留其有关instanceof检查和类型转换的功能保证。

如果您想让我们知道您对这个新功能有何看法,或者如果您对此有任何疑问,请通过常规渠道与我们联系。


返回顶部