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
}
子类(Car
和Bike
)在数据库中拥有独立表存储特有属性。注意:父类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)
)
查询时需要连接多张表。由于Bike
和Car
都继承自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. 一对一组合
一对一组合中,一个实体包含另一个实体的单个实例。通常通过外键关联实现。
以Person
和Address
实体为例,每个人对应一个地址:
@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. 一对多组合
一对多组合中,一个实体包含另一个实体的集合。通过"多"方表中的外键关联"一"方主键。
以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;
// 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)实现关系映射。
以Course
和Student
为例,一门课程有多个学生,一个学生选修多门课程:
@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仓库获取。