Hibernate 中继承映射
1. 概述
关系数据库没有直接的方法将类层次结构映射到数据库表上。
为了解决这个问题,JPA 规范提供了几种策略:
- MappedSuperclass – 父类,不能是实体
- 单表——来自不同类的具有共同祖先的实体被放置在一个表中。
- Joined Table – 每个类都有自己的表,查询子类实体需要加入表。
- 每个类的表——一个类的所有属性都在它的表中,所以不需要连接。
每种策略都会导致不同的数据库结构。
实体继承意味着我们可以在查询超类时使用多态查询来检索所有子类实体。
由于 Hibernate 是一个 JPA 实现,它包含上述所有内容以及一些与继承相关的 Hibernate 特定特性。
在接下来的部分中,我们将更详细地介绍可用的策略。
2. MappedSuperclass
使用MappedSuperclass策略,继承仅在类中明显,而在实体模型中不明显。
让我们首先创建一个代表父类的Person类:
@MappedSuperclass
public class Person {
@Id
private long personId;
private String name;
// constructor, getters, setters
}
请注意,此类不再具有@Entity*注解*,因为它不会自行持久化到数据库中。
接下来,让我们添加一个Employee子类:
@Entity
public class MyEmployee extends Person {
private String company;
// constructor, getters, setters
}
在数据库中,这将对应于一个MyEmployee 表,其中三列用于子类的声明和继承字段。
如果我们使用这种策略,祖先就不能包含与其他实体的关联。
3. 单表
**单表策略为每个类层次结构创建一个表。**如果我们没有明确指定,JPA 也会默认选择这种策略。
我们可以通过将*@Inheritance*注解添加到超类来定义我们想要使用的策略:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyProduct {
@Id
private long productId;
private String name;
// constructor, getters, setters
}
实体的标识符也在超类中定义。
然后我们可以添加子类实体:
@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子类实体。
接下来,我们需要告诉 Hibernate 每个子类记录对于product_type列的值:
@Entity
@DiscriminatorValue("1")
public class Book extends MyProduct {
// ...
}
@Entity
@DiscriminatorValue("2")
public class Pen extends MyProduct {
// ...
}
Hibernate 添加了注解可以采用的另外两个预定义值 — null和not null:
- *@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;
// constructor, getters, setters
}
然后我们可以简单地定义一个子类:
@Entity
public class Pet extends Animal {
private String name;
// constructor, getters, setters
}
两个表都有一个animalId标识符列。
Pet实体的主键对其父实体的主键也有外键约束。
要自定义此列,我们可以添加*@PrimaryKeyJoinColumn*注解:
@Entity
@PrimaryKeyJoinColumn(name = "petId")
public class Pet extends Animal {
// ...
}
这种继承映射方法的缺点是检索实体需要表之间的连接,这会导致大量记录的性能降低。
查询父类时连接的数量更高,因为它将与每个相关的子类连接——因此我们想要检索记录的层次越高,性能就越有可能受到影响。
5. Table per Class
Table per Class 策略将每个实体映射到其表,该表包含实体的所有属性,包括继承的属性。
生成的架构类似于使用 @MappedSuperclass 的架构。但是每个类的表确实会为父类定义实体,从而允许关联和多态查询。
要使用这种策略,我们只需要在基类中添加*@Inheritance*注解即可:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
@Id
private long vehicleId;
private String manufacturer;
// standard constructor, getters, setters
}
然后我们可以以标准方式创建子类。
这与仅映射每个实体而没有继承没有什么不同。查询基类时区别很明显,通过在后台使用UNION语句也将返回所有子类记录。
**在选择此策略时,使用UNION也会导致性能下降。**另一个问题是我们不能再使用身份密钥生成。
6. 多态查询
如前所述,查询基类也将检索所有子类实体。
让我们通过 JUnit 测试来看看这种行为:
@Test
public void givenSubclasses_whenQuerySuperclass_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()).hasSize(2);
}
在此示例中,我们创建了两个Book和Pen对象,然后查询它们的超类MyProduct以验证我们将检索两个对象。
Hibernate 还可以查询不是实体但由实体类扩展或实现的接口或基类。
让我们看看使用我们的*@MappedSuperclass*示例的 JUnit 测试:
@Test
public void givenSubclasses_whenQueryMappedSuperclass_thenOk() {
MyEmployee emp = new MyEmployee(1, "john", "blogdemo");
session.save(emp);
assertThat(session.createQuery(
"from com.blogdemo.hibernate.pojo.inheritance.Person")
.getResultList())
.hasSize(1);
}
请注意,这也适用于任何超类或接口,无论它是否为*@MappedSuperclass*。与通常的 HQL 查询不同的是,我们必须使用完全限定名称,因为它们不是 Hibernate 管理的实体。
如果我们不希望此类查询返回子类,我们只需要在其定义中添加 Hibernate @Polymorphism注解,类型为EXPLICIT:
@Entity
@Polymorphism(type = PolymorphismType.EXPLICIT)
public class Bag implements Item { ...}
在这种情况下,查询Items时,不会返回Bag记录。