Hibernate ORM 6.6 的第一个候选版本已经发布,最终版本也将很快可用。在今天的博客文章中,我将深入探讨一个长期以来一直有人提出但至今未在我们的框架中实现的功能:内嵌继承。

什么是内嵌值

内嵌属性一直是 Hibernate ORM 的一个组成部分,但让我快速介绍一下它们的基本功能。如果您已经对内嵌属性了如指掌,只想了解新闻,请跳转到下一节

通过 @Embeddable 注解标识的内嵌类型是值的组合,与实体不同,它们不直接对应于数据库表。它们的状态与它们作为 @Embedded 属性使用的实体相关联,并且可以在不同的映射中重复使用。内嵌类型也可以通过 @EmbeddedId@IdClass 注解作为复合主键使用,以及作为 @ElementCollection 值。

以下是一个非常简单的由两个属性组成的内嵌类型的示例

@Embeddable
class Animal {
    private int age;

    private String name;
}

以下是使用它的实体映射示例

@Entity
class Owner {
    @Id
    private Long id;

    @Embedded
    private Animal pet;
}

Owner 实体对应的数据库表将包含内嵌的 Animal 属性

create table Owner (
    id bigint not null,
    age integer,
    name varchar(255),
    primary key (id)
)

请注意,内嵌类型可以由基本值以及关联组成。有关内嵌类型提供的所有可能性的更深入信息,您可以参考 Hibernate 用户指南的本章

介绍内嵌继承

Hibernate ORM 中的继承支持,即利用面向对象的基本概念(超类和子类以及域模型中的继承属性)的能力,在历史上一直仅限于实体类型。经过长时间的等待(原始功能请求可追溯到2005年11月!),我们引入了对基于鉴别符列的 @Embeddable 类型继承的支持。

内嵌继承的工作原理

嵌入式继承与单表实体继承类似:一个被@Embeddable注解的类,我们将其称为根类型,可以被其他@Embeddable注解的类扩展,这些类将成为子类型。

embeddable inheritance

在这种情况下,使用该类型的@Embedded属性将在包含它们的实体映射中创建一个额外的列,用于存储复合值特定子类型的信息(除非您使用基于公式的区分器,请参见下一章节)。当检索继承属性时,我们将读取区分器值并实例化正确的具有相应属性的@Embeddable子类型。

以下是一个如何在映射中启用嵌入式继承的示例。以前一章的示例为基础,假设Animal类型被其他嵌入式扩展

@Embeddable
class Mammal extends Animal {
    private String mother;
}

@Embeddable
class Cat extends Mammal {
    // [...]
}

@Embeddable
class Dog extends Mammal {
    // [...]
}

@Embeddable
class Fish extends Animal {
    private int fins;
}

这将启用嵌入式继承,并且在对应该Owner实体,我们将看到所有子类型的属性列以及区分器

create table Owner (
    id bigint not null,
    pet_DTYPE varchar(31) not null,
    age integer,
    name varchar(255),
    mother varchar(255),
    fins integer,
    primary key (id)
)

嵌入式继承也支持用于@ElementCollection的组件。嵌入式继承不支持@EmbeddedId、用作@IdClass的嵌入式类型以及使用自定义@CompositeType的嵌入式属性。

为什么使用嵌入式继承?

嵌入式继承在您希望在映射表中嵌入多态结构而不是依赖整个实体映射本身来处理继承时非常有用。它允许进行清洁和模块化的设计,实现代码重用并保持关注点的清晰分离。

自定义区分器列

默认情况下,区分器列将是字符串类型,名称为<property_name>_DTYPE,其中property_name是相应实体映射中@Embedded属性属性的名称。您可以自定义区分器列映射

  • 对于整个@Embeddable类型,通过使用@DiscriminatorColumn注解的专用列或基于现有属性(使用继承层次结构的根类的@DiscriminatorFormula)的本地SQL表达式来实现(注意:如果在使用同一继承启用的嵌入式类型的同时在同一个实体映射的两个不同属性中使用,这可能会引起列名冲突);

  • 对于特定的@Embedded属性,通过使用具有特殊值({discriminator})的名称的@AttributeOverride注解来实现。

最后,为了为每个子类型指定自定义的区分器值,可以在继承层次结构的类上使用@DiscriminatorValue

专用区分器列

在专用列中存储您的多态嵌入式属性的类型是最简单的解决方案,这使得可以直接通过读取其值轻松理解实体中包含的嵌入式子类型。

以下是一些自定义区分器列和其中存储的值的示例

@Embeddable
@DiscriminatorColumn( name = "animal_type", length = 1 )
static class Animal {
    // [...]
}

@Embeddable
@DiscriminatorValue( "C" )
static class Cat extends Mammal {
    // [...]
}

最后,这是一个示例,说明由持久化具有Cat嵌入式实例的Owner实例触发的插入语句看起来像什么

insert
into
    Owner
    (age, fins, mother, name, animal_type, id)
values
    (3, null, 'Gatta', 'Ariel', 'C', 1)

区分器公式

您还可以通过@DiscriminatorFormula注解将您的多态@Embedded属性类型基于现有列:通过指定一个产生子类型区分器值的本地SQL表达式,您不需要额外的区分器列即可在映射中启用嵌入式继承。

以下是一个基于公式嵌入式多态的非常简单的示例用法

@Embeddable
@DiscriminatorFormula( "case when name like 'cat_%' then 'C' when name like 'dog_%' then 'D' [...] end" )
static class Animal {
    // [...]
}

使用此映射,不需要额外的列来存储嵌入式属性的子类型。以下是一个查询嵌入式属性值的示例

select
    o1_0.id,
    case
        when o1_0.name like 'cat_%' then 'C'
        when o1_0.name like 'dog_%' then 'D'
        -- [...]
    end,
    o1_0.age,
    o1_0.name,
    o1_0.mother,
    o1_0.fins
from
    Owner o1_0

区分器公式非常灵活,允许区分器值从任何本地SQL表达式导出。

支持 type()treat() 操作符

当然,type()treat() 函数也支持内嵌继承,并且可以在查询中显式引用 @Embedded 属性的类型信息。

type()

函数 type() 返回引用实体或内嵌对象的实际类型,即 Java 的 Class

Class<?> petType = entityManager.createQuery( (1)
    "select type(o.pet) " +
    "from Owner o " +
    "where o.id = 1",
    Class.class)
.getSingleResult();

List<Owner> catOwners = entityManager.createQuery( (2)
    "select o " +
    "from Owner o " +
    "where type(o.pet) = Cat",
    Owner.class)
.getResultList();
1 检索内嵌属性的类型
2 将内嵌属性限制为特定的子类型

这允许您在查询中直接与多态内嵌类型交互。

treat()

函数 treat() 可以用于缩小标识变量的类型

List<Owner> owners = entityManager.createQuery(
    "select o " +
    "from Owner o " +
    "where treat(o.pet as Cat).mother = :mother",
    Owner.class)
.setParameter( "mother", mother )
.getResultList();

再次提醒,请参阅用户指南章节类型和类型转换以获取更多详细信息。

优势和局限性

总之,我这里列出了这个新特性的几个优势和局限性

优势:

  • 代码重用性:公共字段在父内嵌类中定义,促进重用并减少冗余。

  • 多态查询:您可以使用 type()treat() 函数有效地处理多态查询。

局限性:

  • 不支持复合键:作为主键使用的内嵌对象不支持继承。

  • 复杂性:管理区分列会增加数据库模式复杂性,尤其是在处理多个内嵌属性时。

展望

Hibernate ORM 中的内嵌继承为我们提供了一种设计干净、模块化和可重用数据模型的额外工具。通过利用区分列,我们的框架允许您无缝地将复杂的继承层次结构映射到关系数据库结构。尽管它增加了复杂性,但就代码可维护性和查询能力而言,其好处通常超过了缺点。

一如既往,我们欢迎对实现新特性的改进请求和讨论。如果您想让我们知道您对这个话题的看法或您有任何问题,请通过常规渠道联系我们。


返回顶部