找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2905

积分

0

好友

387

主题
发表于 4 小时前 | 查看: 3| 回复: 0

在面向对象编程中,继承是一个核心概念。然而,关系型数据库本身并不支持继承。为了解决这一阻抗失配的问题,JPA 规范提供了几种策略,允许我们在实体类之间建立继承关系,并将其映射到数据库表。

本文将详细介绍四种 JPA 实体继承策略,包括 @MappedSuperclassSINGLE_TABLEJOINEDTABLE_PER_CLASS,并通过完整的 Kotlin 代码示例和生成的 SQL 建表语句,帮助你理解它们的区别与适用场景。

1. @MappedSuperclass 继承

这是我们最常用的一种方式。它并非严格意义上的“继承映射”,而更像是一种代码复用机制。

典型场景
假设我们有一个 User(用户)实体和一个 Role(角色)实体。它们都拥有一些共同的字段,例如 idcreatedTime(创建时间)和 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()
}

然后,让 UserRole 实体继承它:

@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) 注解。注意:这种策略通常要求主键生成策略使用 SEQUENCETABLE

代码示例

@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_modelt_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




上一篇:从入门到上手:数据分析自学核心路径与工具方法指南
下一篇:图解Minix文件系统原理:从超级块到目录项的实现解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-15 06:37 , Processed in 0.586923 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表