1. 引言

继承和组合是面向对象编程(OOP)中的两个核心概念,在JPA数据建模中同样适用。虽然两者都用于实体间关系建模,但它们代表完全不同的关系类型。本文将深入探讨这两种方法及其实现细节。

2. JPA中的继承

继承体现"is-a"关系,子类从父类继承属性和行为。通过继承,子类可以复用父类的属性和方法。JPA提供了多种策略来映射实体继承关系到数据库表结构。

2.1. 单表继承(STI)

单表继承(STI)将所有子类映射到同一张数据库表。通过使用鉴别列(discriminator column)区分不同子类实例,简化了模式管理和查询执行。

首先定义Employee实体作为父类,使用@Entity注解。设置继承策略为InheritanceType.SINGLE_TABLE,使所有子类映射到同一张表:

@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;
    
    // Getters and setters
}

使用@DiscriminatorColumn指定鉴别列。本例中列名设为employee_type,类型为字符串:

@Entity
@DiscriminatorValue("manager")
public class Manager extends Employee {
    private String department;
    
    // Getters and setters
}

@Entity
@DiscriminatorValue("developer")
public class Developer extends Employee {
    private String programmingLanguage;
    
    // Getters and setters
}

运行应用时生成的SQL语句示例:

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)

连接表继承(JTI)将子类拆分到独立表中。每个子类拥有专属表存储特有属性,同时存在共享表存储公共属性。

Vehicle父类为例,封装汽车和自行车的公共属性:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Vehicle {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String brand;
    private String model;
    
    // Getters and setters
}

子类(CarBike)在数据库中拥有独立表存储特有属性。注意:父类Vehicle没有独立表,而是作为连接表存储公共信息:

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)
    )

查询时需要连接多张表。由于BikeCar都继承自Vehicle,其id列作为外键引用Vehicle表的id

适用场景:子类属性差异显著时,可减少主表冗余
⚠️ 注意:复杂查询可能导致性能开销

2.3. 每个类一张表继承(TPC)

每个类一张表继承(TPC)使继承层级中的每个类对应独立数据库表。与JTI不同,TPC会将共享属性复制到每个子类表中。

以形状类(圆形和正方形)为例:

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

  // Getters and setters
}

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

  // Getters and setters
}

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

  // Getters and setters
}

生成的表结构:

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中,组合通过实体关系实现(一对一、一对多、多对多)。与继承相比,组合提供更灵活、松耦合的实体关系

3.1. 一对一组合

一对一组合中,一个实体包含另一个实体的单个实例。通常通过外键关联实现。

PersonAddress实体为例,每个人对应一个地址:

@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;
    
    // Getters and setters
}

Address实体存储地址详情:

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String street;
    private String city;
    private String zipCode;
    
    // Getters and setters
}

组合的优势在此体现:若需向Address添加新字段(如国家),可独立修改无需触碰Person实体。而在继承关系中,父类变更可能影响所有子类。

3.2. 一对多组合

一对多组合中,一个实体包含另一个实体的集合。通过"多"方表中的外键关联"一"方主键。

DepartmentEmployee为例,一个部门包含多个员工:

@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;
    
    // Getters and setters
}

Employee实体通过@ManyToOne注解引用所属部门:

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;
    
    // Getters and setters
}

3.3. 多对多组合

多对多组合中,双方实体都包含对方的集合。通过关联表(join table)实现关系映射。

CourseStudent为例,一门课程有多个学生,一个学生选修多门课程:

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToMany(mappedBy = "courses")
    private List<Student> students;
    
    // Getters and setters
}

@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;
    
    // Getters and setters
}

4. 核心差异对比

维度 继承 组合
关系本质 "is-a"关系(父子关系) "has-a"关系(包含关系)
代码复用 层级内复用,子类继承父类行为 组件可在不同上下文复用
灵活性 父类变更可能影响所有子类 组件变更不影响包含对象
耦合度 高耦合,子类依赖父类实现细节 松耦合,组件与容器解耦

5. 结论

在JPA实体建模中,继承和组合各有适用场景:

  • 继承优势:代码复用性强,层级结构清晰,适合子类共享大量公共属性/行为的场景
  • 组合优势:灵活性高,动态组装对象,降低组件间依赖

简单粗暴的选择建议:

  • ✅ 当实体间存在明确的"父子"关系且共享大量属性时,优先考虑继承
  • ✅ 当实体间是"包含"关系或需要灵活变更时,组合是更安全的选择

实战踩坑提醒:过度使用继承可能导致数据库设计僵化,而组合过度使用可能增加查询复杂度。根据实际业务场景权衡取舍才是王道。

本文示例代码可在GitHub仓库获取。


原始标题:Inheritance vs. Composition in JPA