1. 概述

关系型数据库没有直接映射类层次结构到数据库表的简单方法。为解决这个问题,JPA 规范提供了几种策略:

  • MappedSuperclass - 父类不能作为实体
  • 单表策略 - 不同类但具有共同祖先的实体存放在同一张表
  • 连接表策略 - 每个类对应一张表,查询子类实体需要连接多张表
  • 每类一表策略 - 类的所有属性都在其表中,无需连接

每种策略都会产生不同的数据库结构。实体继承意味着查询父类时,可以使用多态查询检索所有子类实体。Hibernate 作为 JPA 实现,不仅支持上述所有策略,还提供了一些 Hibernate 特有的继承特性。接下来我们详细探讨这些策略。

2. MappedSuperclass 策略

使用 MappedSuperclass 策略时,继承关系仅体现在类层面,而非实体模型中。先创建一个父类 Person

@MappedSuperclass
public class Person {

    @Id
    private long personId;
    private String name;

    // 构造器、getter、setter
}

⚠️ 注意:这个类没有 @Entity 注解,因为它本身不会被持久化到数据库。

接着添加子类 MyEmployee

@Entity
public class MyEmployee extends Person {
    private String company;
    // 构造器、getter、setter 
}

在数据库中,这会生成一张 MyEmployee 表,包含子类声明和继承的所有字段对应的列。使用此策略时,父类不能包含与其他实体的关联关系。

3. 单表策略

单表策略为整个类层次结构创建一张表。若未显式指定,JPA 默认采用此策略。通过在父类添加 @Inheritance 注解定义策略:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyProduct {
    @Id
    private long productId;
    private String name;

    // 构造器、getter、setter
}

实体的标识符在父类中定义。然后添加子类实体:

@Entity
public class Book extends MyProduct {
    private String author;
}
@Entity
public class Pen extends MyProduct {
    private String color;
}

3.1. 鉴别器值

由于所有实体记录都在同一张表中,Hibernate 需要一种方式区分它们。默认通过名为 DTYPE 的鉴别器列实现,其值为实体名称。使用 @DiscriminatorColumn 注解可自定义鉴别器列:

@Entity(name="products")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="product_type", 
  discriminatorType = DiscriminatorType.INTEGER)
public class MyProduct {
    // ...
}

这里我们选择用名为 product_type 的整型列区分 MyProduct 子类实体。接下来需指定每个子类在 product_type 列的值:

@Entity
@DiscriminatorValue("1")
public class Book extends MyProduct {
    // ...
}
@Entity
@DiscriminatorValue("2")
public class Pen extends MyProduct {
    // ...
}

Hibernate 还支持两个预定义值:

  • @DiscriminatorValue("null") - 无鉴别器值的行映射到带此注解的实体类(通常用于根类)
  • @DiscriminatorValue("not null") - 鉴别器值不匹配任何实体定义的行映射到带此注解的类

也可使用 Hibernate 特有的 @DiscriminatorFormula 注解通过公式计算区分值:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("case when author is not null then 1 else 2 end")
public class MyProduct { ... }

优势:多态查询性能好,查询父类实体只需访问单张表
劣势:子类实体属性无法使用 NOT NULL 约束

4. 连接表策略

此策略将层次结构中的每个类映射到独立表,唯一共同列是标识符(用于必要时连接表)。先创建父类:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal {
    @Id
    private long animalId;
    private String species;

    // 构造器、getter、setter 
}

然后定义子类:

@Entity
public class Pet extends Animal {
    private String name;

    // 构造器、getter、setter
}

两张表都有 animalId 标识符列,且 Pet 实体的主键会作为外键关联到父实体主键。使用 @PrimaryKeyJoinColumn 可自定义此列:

@Entity
@PrimaryKeyJoinColumn(name = "petId")
public class Pet extends Animal {
    // ...
}

劣势:检索实体需连接多表,大量记录时性能可能下降。查询父类时连接操作更多(需连接所有子表),层次结构越高性能影响越大。

5. 每类一表策略

每类一表策略将每个实体映射到独立表,表中包含该实体的所有属性(包括继承的属性)。生成的模式与 @MappedSuperclass 类似,但此策略会为父类定义实体,从而支持关联和多态查询。只需在基类添加 @Inheritance 注解:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
    @Id
    private long vehicleId;

    private String manufacturer;

    // 标准构造器、getter、setter
}

然后按标准方式创建子类。这与无继承的独立实体映射区别不大,但查询基类时,Hibernate 会在后台使用 UNION 语句返回所有子类记录。

劣势UNION 的使用可能导致性能下降,且无法使用标识符(identity)主键生成策略。

6. 多态查询

如前所述,查询父类会同时检索所有子类实体。通过 JUnit 测试验证:

@Test
public void givenSubclasses_whenQuerySingleTableSuperclass_thenOk() {
    Book book = new Book(1, "1984", "George Orwell");
    session.save(book);
    Pen pen = new Pen(2, "my pen", "blue");
    session.save(pen);

    assertThat(session.createQuery("from MyProduct")
      .getResultList()).size()).isEqualTo(2);
}

此例创建 BookPen 对象后查询父类 MyProduct,验证返回两个对象。Hibernate 也能查询非实体但被实体类扩展/实现的接口或基类。以 @MappedSuperclass 为例:

@Test
public void givenSubclasses_whenQueryMappedSuperclass_thenOk() {
    MyEmployee emp = new MyEmployee(1, "john", "baeldung");
    session.save(emp);

    assertThat(session.createQuery(
      "from com.baeldung.hibernate.pojo.inheritance.Person")
      .getResultList())
      .hasSize(1);
}

⚠️ 注意:需使用全限定名(因非 Hibernate 管理实体)。若不希望某子类被此类查询返回,添加 @Polymorphism(type = PolymorphismType.EXPLICIT) 注解:

@Entity
@Polymorphism(type = PolymorphismType.EXPLICIT)
public class Bag implements Item { ...}

此时查询 Items 会抛出异常(因 Bag 被显式排除)。需定义另一个带 @Polymorphism(type = PolymorphismType.IMPLICIT) 的类,查询 Items 时才会同时搜索实现该接口的两个对象。

7. 总结

本文详细介绍了 Hibernate 中映射继承关系的各种策略。每种策略各有优劣:

策略 优势 劣势
MappedSuperclass 简单共享字段 父类不能作为实体,无法关联
单表策略 多态查询性能最佳 子类属性不能有 NOT NULL 约束
连接表策略 模式规范化,支持约束 查询需多表连接,性能可能较差
每类一表策略 无需连接,支持实体关联 UNION 操作影响性能,不支持标识符主键

踩坑提醒:选择策略时需权衡查询性能与数据完整性约束。单表策略适合层次结构简单且子类字段不多的场景,而连接表策略更适合规范化要求高的场景。

示例完整源码可在 GitHub 获取。


原始标题:Hibernate Inheritance Mapping