在面向对象编程中,继承是一个核心概念。然而,关系型数据库本身并不支持继承。为了解决这一阻抗失配的问题,JPA 规范提供了几种策略,允许我们在实体类之间建立继承关系,并将其映射到数据库表。
本文将详细介绍四种 JPA 实体继承策略,包括 @MappedSuperclass、SINGLE_TABLE、JOINED 和 TABLE_PER_CLASS,并通过完整的 Kotlin 代码示例和生成的 SQL 建表语句,帮助你理解它们的区别与适用场景。
1. @MappedSuperclass 继承
这是我们最常用的一种方式。它并非严格意义上的“继承映射”,而更像是一种代码复用机制。
典型场景:
假设我们有一个 User(用户)实体和一个 Role(角色)实体。它们都拥有一些共同的字段,例如 id、createdTime(创建时间)和 lastModifiedTime(最后修改时间)。我们可以声明一个抽象基类,并使用 @MappedSuperclass 注解来定义这些公共字段。其他实体只需继承这个基类,就能在代码层面获得这些属性,并且在数据库表中也会生成对应的字段。
实现步骤:
首先,声明一个抽象基类 AbstractModel:
@MappedSuperclass
abstract class AbstractModel {
@Id
@Column(name = "id_")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
@Column(name = "created_time_")
val cratedTime: ZonedDateTime = ZonedDateTime.now()
@Column(name = "last_modified_time_")
var lastModifiedTime: ZonedDateTime = ZonedDateTime.now()
}
然后,让 User 和 Role 实体继承它:
@Entity
@Table(name = "t_user_")
class User : AbstractModel() {
@Column(name = "username_", length = 50)
var username: String = ""
@Column(name = "password_", length = 64)
var password: String = ""
}
@Entity
@Table(name = "t_role_")
class Role : AbstractModel() {
@Column(name = "name_", length = 50)
var name: String = ""
}
生成的表结构:
create table t_role_ (
id_ bigint generated by default as identity,
created_time_ timestamp(6) with time zone,
last_modified_time_ timestamp(6) with time zone,
name_ varchar(50),
primary key (id_)
);
create table t_user_ (
id_ bigint generated by default as identity,
created_time_ timestamp(6) with time zone,
last_modified_time_ timestamp(6) with time zone,
password_ varchar(64),
username_ varchar(50),
primary key (id_)
);
策略效果总结:
AbstractModel 本身不会生成数据库表。
t_user_ 和 t_role_ 表中都会包含基类中定义的 id_、created_time_ 和 last_modified_time_ 字段。
- 每个实体表的 ID 生成策略是独立的,实体之间没有直接的数据库关联。
2. 单表继承 (SINGLE_TABLE)
在这种策略下,父类和所有子类的数据都存储在同一张数据库表中。表中会通过一个特殊的鉴别器列(dtype)来区分每一行数据属于哪个子类。只需在父类上使用 @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 注解即可。
代码示例:
父类 AbstractModel 是一个实体,并声明继承策略:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
open class AbstractModel {
@Id
@Column(name = "id_")
@GeneratedValue(strategy = GenerationType.IDENTITY)
open val id: Long? = null
@Column(name = "created_time_")
open val cratedTime: ZonedDateTime = ZonedDateTime.now()
@Column(name = "last_modified_time_")
open var lastModifiedTime: ZonedDateTime = ZonedDateTime.now()
}
@Entity
class Role : AbstractModel() {
@Column(name = "name_", length = 50)
var name: String = ""
}
@Entity
class User : AbstractModel() {
@Column(name = "username_", length = 50)
var username: String = ""
@Column(name = "password_", length = 64)
var password: String = ""
}
生成的建表语句:
create table abstract_model (
dtype varchar(31) not null check ((dtype in ('AbstractModel','Role','User'))),
id_ bigint generated by default as identity,
created_time_ timestamp(6) with time zone,
last_modified_time_ timestamp(6) with time zone,
name_ varchar(50),
password_ varchar(64),
username_ varchar(50),
primary key (id_)
);
策略效果总结:
- 只有父类
AbstractModel 会生成一张表(abstract_model)。
- 子类不能使用
@Table 注解来指定表名。
- 表中自动添加
dtype 列用于标识记录类型。
- 所有子类独有的字段(如
name_, username_, password_)都会被添加到父类表中。对于某条具体记录,不属于其类型的字段值为 NULL。
3. 连接表继承 (JOINED)
这种策略下,父类和每个子类都拥有自己的数据库表,子类表只包含自己独有的字段,并通过主键与父类表进行连接(JOIN)来获取完整的继承属性。在父类上使用 @Inheritance(strategy = InheritanceType.JOINED) 注解。
代码示例:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
open class AbstractModel {
@Id
@Column(name = "id_")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
@Column(name = "created_time_")
val cratedTime: ZonedDateTime = ZonedDateTime.now()
@Column(name = "last_modified_time_")
var lastModifiedTime: ZonedDateTime = ZonedDateTime.now()
}
@Entity
@Table(name = "t_role_")
class Role : AbstractModel() {
@Column(name = "name_", length = 50)
var name: String = ""
}
@Entity
@Table(name = "t_user_")
class User : AbstractModel() {
@Column(name = "username_", length = 50)
var username: String = ""
@Column(name = "password_", length = 64)
var password: String = ""
}
生成的表结构:
create table abstract_model (
id_ bigint generated by default as identity,
created_time_ timestamp(6) with time zone,
last_modified_time_ timestamp(6) with time zone,
primary key (id_)
);
create table t_role_ (
name_ varchar(50),
id_ bigint not null,
primary key (id_)
);
create table t_user_ (
password_ varchar(64),
username_ varchar(50),
id_ bigint not null,
primary key (id_)
);
alter table if exists t_role_ add constraint FKs5f8aqfjxmnt60k5w5ecfatpn foreign key (id_) references abstract_model;
alter table if exists t_user_ add constraint FK1m8egvri3dc7pswqyhis6jmpg foreign key (id_) references abstract_model;
策略效果总结:
- 父类
AbstractModel 生成表,包含公共字段。
- 每个子类生成各自的表(
t_role_, t_user_),但只包含其独有的字段。
- 子类表与父类表通过一对一的外键关系关联,共享主键
id_。
- 子表的 ID 由父表生成和管理,并通过外键约束确保一致性。
4. 每个类一个表继承 (TABLE_PER_CLASS)
这种模式下,包括父类在内的每一个实体类都会生成一张独立的、包含其全部字段(包括继承来的)的表。在父类上使用 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 注解。注意:这种策略通常要求主键生成策略使用 SEQUENCE 或 TABLE。
代码示例:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
open class AbstractModel {
@Id
@Column(name = "id_")
@GeneratedValue(strategy = GenerationType.SEQUENCE)
open val id: Long? = null
@Column(name = "created_time_")
open val cratedTime: ZonedDateTime = ZonedDateTime.now()
@Column(name = "last_modified_time_")
open var lastModifiedTime: ZonedDateTime = ZonedDateTime.now()
}
@Entity
@Table(name = "t_role_")
class Role : AbstractModel() {
@Column(name = "name_", length = 50)
var name: String = ""
}
@Entity
@Table(name = "t_user_")
class User : AbstractModel() {
@Column(name = "username_", length = 50)
var username: String = ""
@Column(name = "password_", length = 64)
var password: String = ""
}
生成的建表语句:
create table abstract_model (
id_ bigint not null,
created_time_ timestamp(6) with time zone,
last_modified_time_ timestamp(6) with time zone,
primary key (id_)
);
create table t_role_ (
id_ bigint not null,
created_time_ timestamp(6) with time zone,
last_modified_time_ timestamp(6) with time zone,
name_ varchar(50),
primary key (id_)
);
create table t_user_ (
id_ bigint not null,
created_time_ timestamp(6) with time zone,
last_modified_time_ timestamp(6) with time zone,
password_ varchar(64),
username_ varchar(50),
primary key (id_)
);
策略效果总结:
- 父类
AbstractModel 会生成表。
- 每个子类都会生成表,并且表中重复包含所有从父类继承来的字段。
- 子类表和父类表之间没有外键关联。
- 所有相关表共享一个 ID 序列空间,这意味着
abstract_model、t_role_ 和 t_user_ 表中的 id_ 值不能重复。
小结:四种继承策略对比
为了帮助你快速做出选择,下表总结了四种继承策略的核心差异:
| 继承方式 |
创建父表 |
创建子表 |
子表包含所有字段 |
共享ID空间 |
| @MappedSuperclass |
否 |
是 |
是 |
否 |
| SINGLE_TABLE |
是 |
否 |
否 |
是 |
| TABLE_PER_CLASS |
是 |
是 |
是 |
是 |
| JOINED |
是 |
是 |
否 |
是 |
如何选择?
- @MappedSuperclass:适用于纯粹的代码复用场景,子类之间无需多态查询。
- SINGLE_TABLE:查询效率最高(无 JOIN),但可能产生大量
NULL 字段,子类字段不宜过多。
- JOINED:表结构最规范,消除了数据冗余,但查询性能相对较差(需要 JOIN),是最符合关系型数据库设计范式的选择。
- TABLE_PER_CLASS:某些数据库(如 MySQL)对使用
IDENTITY 主键生成策略支持不佳,且多态查询会使用 UNION ALL 导致性能低下,一般较少使用。
理解 JPA 的这些继承映射策略,对于设计清晰、高效的领域模型至关重要。希望本文的详细对比能帮助你在实际项目中做出最适合的技术选型。
本文示例项目地址:https://github.com/ldwqh0/jpa-demo.git