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);
}
此例创建 Book
和 Pen
对象后查询父类 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 获取。