概述

在关系型数据库中我们没有直接的方法去映射类的继承到数据库表中。为了解决这个问题,JPA 标准提供了几种策略:

  • MappedSuperclass - 父类,不能是实体
  • Single Table - 来自具有共同祖先的不同类的实体被放置在单个表中
  • Joined Table - 每个类都有自己的表,查询子类实体需要连接表
  • Table-Per-Class - 类的所有的属性都在一张表中,所以不需要连接 每种策略都会产生不同的数据库结构。

实体查询意味着我们可以在查询父类时使用动态查询获取所有的子类。 由于 Hibernate 是 JPA 的一个实现,它包含上述所有内容以及一些与继承相关的特定于 Hibernate 的功能。

映射父类策略

使用 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” 表,总共三列包含子类定义的字段以及继承而来的字段。

**如果我们使用此策略,祖先不能包含与其他实体的关联。 该策略可以用于为相同类定义相同字段,比如创建时间,创建者等审计功能或者通用字段。 **

单表策略

单表策略为每个类层次结构创建一个表。 如果我们没有明确指定,这也是 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;
}

这将生成下面的表:

create table MyProduct (
  DTYPE varchar(31) not null,
  productId bigint not null,
  author varchar(255),
  color varchar(255),
  primary key (productId)
)

当我们向其中插入数据时:

Book book = new Book();
book.setProductId(1L);
book.setAuthor("Zeral");

Pen pen = new Pen();
pen.setProductId(2L);
pen.setColor("red");

Hibernate 将会使用子类名称填充 DTYPE

INSERT INTO MyProduct (author, DTYPE, productId) VALUES ('Zeral', 'Book', 1);

INSERT INTO MyProduct (color, DTYPE, productId) VALUES ('red', 'Pen', 2);

Discriminator Values 鉴别器值

由于所有的实体记录都在同一个表中,因此 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”) - 当任意行有鉴别器值时但是未找到该鉴别器对应的类时将被映射到使用此注解的实体类

这两个注解可以解决在集成遗留数据库时,鉴别器列包含 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 约束。 **

连表策略

使用此策略,层次结构中的每个类都映射到其表。在所有表中重复出现的唯一列是标识符,将在需要时用于连接它们。 让我们创建一个使用此策略的父类:

@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 {
    // ...
}

创建的表结构如下:

create table Animal (
  animalId bigint not null,
  species varchar(255),
  primary key (animalId);
)

create table Pet (
  petId bigint not null,
  name varchar(255),
  primary key (petId),
  foreign key(petId) references Animal(animalId) 
)

此继承映射方法的缺点是检索实体需要表之间的连接,这可能导致大量记录的性能降低。 查询父类时,连接数会更高,因为它将与每个相关的子项连接 - 因此,我们想要检索记录的层次结构越高,性能就越可能受到影响。

单类单表策略

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
}

我们为其添加一个子类:

public class Car extends Vehicle {
  private String engine;
  
  // standard constructor, getters, setters
}

生成的 sql 如下:

CREATE TABLE Car (
  vehicleId bigint(20) NOT NULL,
  manufacturer varchar(255) DEFAULT NULL,
  engine varchar(255) DEFAULT NULL,
  PRIMARY KEY (vehicleId)
);

这与在没有继承的情况下映射每个实体没有太大区别。在查询基类时,这种区别是显而易见的,它将在后台使用 UNION 语句返回所有子类记录。 在选择此策略时,使用 UNION 也会导致性能下降。另一个问题是我们不能再使用标式键密钥生成。

多态查询

我们创建两个 Book 和 Pen 对象,然后查询它们的超类 MyProduct 以验证我们将返回两个对象。 Hibernate 还可以查询由实体类扩展或实现的接口或基类这些非实体类。

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

请注意,这也适用于任何父类或接口,无论它是否是 @MappedSuperclass。与通常的 HQL 查询的区别在于我们必须使用完全限定名称,因为它们不是由 Hibernate 管理的实体。 如果我们不希望这种类型的查询返回特定子类,那么我们只需要在其定义中添加 Hibernate @Polymorphism 注释,类型为 EXPLICIT

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

在这种情况下,查询父类时,不会返回 Bag 记录。