JPA 中实体的继承与组合

1、简介

继承(Inheritance)和组合(Composition)是面向对象编程(OOP)中的两个基本概念,我们也可以在 JPA 中利用它们进行数据建模。在 JPA 中,继承和组合都是对实体间关系进行建模的技术,但它们代表的是不同类型的关系。本文将带你了解这两种方法及其影响。

2、JPA 中的继承

继承是一种 “is-a” 关系,即子类继承超类的属性和行为。这允许子类从超类继承属性和方法,从而促进了代码的重用。JPA 提供了几种策略来模拟实体与其对应的数据库表之间的继承关系。

2.1、单表继承(STI)

单表继承(Single Table Inheritance,STI)将所有子类映射到单个数据库表中。通过利用 区分列 来区分子类实例,这简化了 Schema 管理和查询执行过程。

首先,使用 @Entity 注解将 Employee 实体类定义为超类。接下来,将继承策略设置为 InheritanceType.SINGLE_TABLE,这样所有子类都会映射到同一个数据库表*。

然后,使用 @DiscriminatorColumn 注解来指定 Employee 类中的 区分列。该列用于区分单个表中不同类型的实体。

示例如下,使用 name = "employee_type" 将列名称指定为 employee_type,并使用 discriminatorType = DiscriminatorType.STRING 表示列包含字符串值:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "employee_type", discriminatorType = DiscriminatorType.STRING)
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    // Get / Set 方法省略
}

对于每个子类,使用 @DiscriminatorValue 注解来指定与该子类相对应的 区别列 的值。在本例中,我们使用 managerdeveloper 分别作为 ManagerDeveloper 子类的 区别值

@Entity
@DiscriminatorValue("manager")
public class Manager extends Employee {
    private String department;
    
    // Get / Set 方法省略
}

@Entity
@DiscriminatorValue("developer")
public class Developer extends Employee {
    private String programmingLanguage;
    
    // Get / Set 方法省略
}

启动应用,自动创建数据表。 DDL 语句如下:

Hibernate: 
    create table Employee (
        id bigint generated by default as identity,
        employee_type varchar(31) not null,
        department varchar(255),
        name varchar(255),
        programmingLanguage varchar(255),
        primary key (id)
    )

这种方法对于大多数子类共享相似属性的继承层次结构是理想的,它减少了表的创建和查询次数。然而,随着层次结构的扩展,这可能会导致稀疏表和潜在的性能问题。

2.2、连接表继承(JTI)

连接表继承(Joined Table Inheritance,JTI)将子类分割成不同的表。每个子类都有自己的表来存储其独特的详细信息。此外,还有一个表用于保存所有子类共有的共享信息。

用超类 Vehicle (车)来说明这个概念,它封装了汽车和自行车的共同属性:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Vehicle {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String brand;
    private String model;
    
    // Get / Set 方法省略
}

随后,定义 CarBike 子类,每个子类都针对其特定的属性进行了定制:

@Entity
public class Car extends Vehicle {
    private int numberOfDoors;
    
    // Get / Set 方法省略
}

@Entity
public class Bike extends Vehicle {
    private boolean hasBasket;
    
    // Get / Set 方法省略
}

在此设置中,每个子类(CarBike)在数据库中都有自己的表,以容纳其独特的属性。然而,Vehicle 超类并不拥有专用表。相反,Vehicle 作为一个独立的表,用于存储所有子类共享的 brandmodel 等通用信息:

Hibernate: 
    create table Bike (
        hasBasket boolean not null,
        id bigint not null,
        primary key (id)
    )

Hibernate: 
    create table Car (
        numberOfDoors integer not null,
        id bigint not null,
        primary key (id)
    )

Hibernate: 
    create table Vehicle (
        id bigint generated by default as identity,
        brand varchar(255),
        model varchar(255),
        primary key (id)
    )

在查询数据时,可能需要将表连接起来,以检索特定车辆(Vehicle)的信息。由于 BikeCar 都从超类表(Vehicle)继承 id,因此 CarBike 中的 id 列都将是引用 Vehicleid 的外键。

这种方法适用于属性差异较大的子类,可最大限度地减少主表的冗余。

2.3、类对应表(TPC)

在 JPA 中使用 “类对应表”(TPC)继承策略时,继承层次结构中的每个类都对应其专用的数据库表。因此,如下例,每个类都创建了单独的表: ShapeSquareCircle。这与 JTI 不同,在 JTI 中,每个子类都有自己的表来存储其独特的详细信息,而共享属性则存储在一个联合表中。

考虑一个包含 CircleSquare 等形状的场景。我们可以使用 “类对应表”(TPC)来建模这个场景:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Shape {
  @Id
  private Long id;
  private String color;

  // Get / Set 方法省略
}

@Entity
public class Circle extends Shape {
  private double radius;

  // Get / Set 方法省略
}

@Entity
public class Square extends Shape {
  private double sideLength;

  // Get / Set 方法省略
}

Shape 表中,创建了 idcolor 列,代表 SquareCircle 继承的共享属性:

Hibernate: 
    create table Shape (
        id bigint not null,
        color varchar(255),
        primary key (id)
    )
Hibernate: 
    create table Square (
        sideLength float(53) not null,
        id bigint not null,
        color varchar(255),
        primary key (id)
    )
Hibernate: 
    create table Circle (
        radius float(53) not null,
        id bigint not null,
        color varchar(255),
        primary key (id)
    )

虽然 TPC 继承提供了类与表之间的清晰映射,但它会将所有共享属性继承到子类的表中。这种方法会导致数据冗余,因为每个子类表都重复了从超类表中继承的列。这种冗余会导致数据库容量增大,存储需求增加。

此外,更新超类中的共享属性可能需要对多个表进行修改,这会给维护工作带来额外的麻烦。

3、JPA 中的组合

组合表示一种 “has-a” 关系,即一个对象包含另一个对象作为组件部分。在 JPA 中,组合通常使用实体关系来实现,如一对一、一对多或多对多关联。与继承相比,组合允许实体间更灵活、更松散的耦合关系。

接下来,让我们通过示例来说明 JPA 中每种类型的组合关系。

3.1、一对一

在一对一组合关系中,一个实体包含另一个实体的一个实例作为其组成部分。通常,可以使用拥有实体表中的外键来引用相关实体的主键来建模这种关系。

考虑一个与 Address 实体有一对一组合关系的 Person 实体。每个人都有一个地址:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_id")
    private Address address;
    
    // Get / Set 方法省略
}

Address 实体表示物理位置的详细信息:

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String street;
    private String city;
    private String zipCode;
    
    // Get / Set 方法省略
}

在这种情况下,如果我们决定在 Address 实体中添加一个新字段(如 country,国家),可以独立完成,而无需对 Person 实体进行任何更改。但是,在继承关系中,在超类中添加一个新字段可能确实需要修改子类表。这就突出了组合比继承在灵活性和易维护性方面的优势之一。

3.2、一对多

在一对多的组合关系中,一个实体包含另一个实体的实例集合(Collection)作为其部分。这种关系通常使用 “多” 方实体表中的外键来引用 “一” 方实体的主键。

假设有一个 Department 实体,它与 Employee 实体之间是一对多的组合关系。每个部门可以有多个员工:

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
    private List<Employee> employees;
    
    // Get / Set 方法省略
}

Employee 实体包含对所属 Department 实体的引用。Employee 实体中 department 字段上的 @ManyToOne 注解表示了这一点:

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;
    
    // Get / Set 方法省略
}

3.3、多对多

在多对多组合关系中,双方的实体都包含另一实体的实例集合(Collection)作为其组成部分。这种关系通常使用数据库中的连接表来建模,以表示实体间的关联。

来看看 Course 实体与 Student 实体之间的多对多组成关系。每个课程可以有多个学生,每个学生可以注册多个课程。对 CourseStudent 实体使用 @ManyToMany 注解来对此进行建模:

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToMany(mappedBy = "courses")
    private List<Student> students;
    
    // Get / Set 方法省略
}

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private List<Course> courses;
    
    // Get / Set 方法省略
}

4、继承和组合的对比

下表展示了继承和组合的主要区别,包括它们的关系性质、代码重用性、灵活性和耦合性:

对比面 继承 组合
关系性质 表示 “is-a” 关系 表示 “has-a” 关系
代码重用性 便于在层次结构中重复使用代码。子类继承超类的行为和属性。 组件可以在不同的上下文中重复使用,而不会出现继承所固有的高耦合
灵活性 更改超类可能会影响所有子类,从而可能导致级联更改 对单个组件的更改不会影响包含的对象
耦合 类之间的紧密耦合。子类与超类的实现细节紧密相连 松耦合。组件与包含的对象解耦,减少了依赖性

5、总结

本文介绍了 JPA 实体建模中继承和组合的根本区别。

继承提供了代码重用性和清晰的层次结构,使其适用于子类共享共同行为和属性的场景。而,组合提供了更大的灵活性和适应性,允许动态对象组装并减少组件之间的依赖性。


Ref:https://www.baeldung.com/jpa-inheritance-vs-composition