从零开始 Spring Boot 60:一个实体映射到多个表
图源:简书 (jianshu.com)
在之前的文章中我们讨论了 JPA 中的一对一关系,实际上存在一种特殊的一对一关系,即将一个实体映射到多张表,本文会讨论这种关系。
我之前提过,有时候会因为性能上的考量将一张表拆分成多张表,虽然拆分后也可以用一对一关系来表示和实现,但这样并不是特别合适,因为一对一关系中一边的关系是可以为null
的,比如说一个学生对应一个电子邮件地址,也可能有的学生没有电子邮件地址,此时email
表可能就没有一条对应的数据。但如果是从同一张表上拆分出的两张表,必然存在一对一的关系,即使另一张表中的数据都是null
字段。
下面我们将说明如何实现这种关系。
示例
假设我们有一张表,保存学生的所有信息,这个表可以用下面的实体类表示:
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
private String name;
@NotNull
private Boolean loveMusic = false;
@NotNull
private Boolean loveDraw = false;
private String address;
private String email;
}
Hibernate 生成的表结构如下:
CREATE TABLE `student` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`address` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`love_draw` bit(1) NOT NULL,
`love_music` bit(1) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
假如这个表有性能瓶颈,我们需要将其中的爱好相关字段拆分出来,但我们又不希望改变对这个实体的使用习惯,即我们依然希望通过这个实体的属性访问相关数据。
@SecondaryTable
为了实现这个目的,可以按下面的方式修改实体类:
@Entity(name = "Student2")
@Table(name = "student2")
@SecondaryTable(name = "student_hobbies2",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "student_id"))
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
private String name;
@NotNull
@Column(table = "student_hobbies2")
private Boolean loveMusic = false;
@NotNull
@Column(table = "student_hobbies2")
private Boolean loveDraw = false;
private String address;
private String email;
}
这里用@SecondaryTable
注解来表示要拆分出去的副表,name
表示副表的名称,pkJoinColumns
表示用于关联两张表的主键(外键)。
此外,还需要将拆分出去的字段相关的属性的@Column
注解标注为副表字段(table=xxx
)。
现在 Hibernate 生成的表结构为:
CREATE TABLE `student2` (
`id` bigint NOT NULL AUTO_INCREMENT,
`address` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `student_hobbies2` (
`love_draw` bit(1) NOT NULL,
`love_music` bit(1) NOT NULL,
`student_id` bigint NOT NULL,
PRIMARY KEY (`student_id`),
CONSTRAINT `FKp37nmekoeubhhvwpsw0euvps3` FOREIGN KEY (`student_id`) REFERENCES `student2` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
但对于实体的调用方来说,不会有任何影响,依然可以用之前的方式使用实体类,甚至不会“察觉”这个实体中的字段已经拆分成了两张表,这就是实体抽象层的好处。
@SecondaryTables
可以用多个@SecondaryTable
让一个实体对应多张表:
// ...
@SecondaryTable(name = "student_hobbies3",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "student_id"))
@SecondaryTable(name = "student_detail3",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "student_id"))
public class Student {
// ...
@NotNull
@Column(table = "student_hobbies3")
private Boolean loveMusic = false;
@NotNull
@Builder.Default
@Column(table = "student_hobbies3")
private Boolean loveDraw = false;
@Column(table = "student_detail3")
private String address;
@Column(table = "student_detail3")
private String email;
}
JDK8 之前的版本不支持重复注解,可以这样:
@SecondaryTables({
@SecondaryTable(name = "student_hobbies3",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "student_id")),
@SecondaryTable(name = "student_detail3",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "student_id"))})
public class Student {
// ...
}
@Embeddable 和 @Embedded
之前我们介绍过@Embeddable
和@Embedded
的用途,映射到副表的属性同样可以用类似的方式进行分组:
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "Student4")
@Table(name = "student4")
@SecondaryTable(name = "student_hobbies4",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "student_id"))
@SecondaryTable(name = "student_detail4",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "student_id"))
public class Student {
@ToString
@Embeddable
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Hobbies {
@NotNull
@Builder.Default
@Column(table = "student_hobbies4")
private Boolean loveMusic = false;
@NotNull
@Builder.Default
@Column(table = "student_hobbies4")
private Boolean loveDraw = false;
}
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public static class Details {
@Column(table = "student_detail4")
private String address;
@Column(table = "student_detail4")
private String email;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
@Column(unique = true)
private String name;
@NotNull
@Embedded
@Builder.Default
private Hobbies hobbies = new Hobbies();
@NotNull
@Embedded
@Builder.Default
private Details details = new Details();
}
如果@Embeddable
标记的类需要重用,也可以使用@AttributeOverrides
注解,具体可以看这篇文章。
The End,谢谢阅读。
本文的完整示例可以从这里获取。
参考资料
- 从零开始 Spring Boot 56:JPA中的一对一关系 - 红茶的个人站点 (icexmoon.cn)
- Mapping a Single Entity to Multiple Tables in JPA