图源:简书 (jianshu.com)
关系型数据库设计中是不存在继承概念的,但实体类可以用继承来组织代码结构,所以需要用一种方式将实体类的继承结构映射到表结构。
本文将介绍几种在 JPA(Hibernate)中映射实体类继承层次的方式。
第一种方式是用@MappedSuperclass
标记超类(Super Class),超类并不对应任何表结构,而是体现在子类对应的表中都拥有超类的字段(每个子类对应一张表)。
@ToString
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@MappedSuperclass
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
@Column(unique = true)
private String name;
public Person(String name) {
this.name = name;
}
}
@ToString(callSuper = true)
@Getter
@Setter
@NoArgsConstructor
@Entity(name = "student")
public class Student extends Person {
@Min(0)
@Max(100)
@NotNull
private Integer averageScore;
public Student(String name, Integer averageScore) {
super(name);
this.averageScore = averageScore;
}
}
@ToString(callSuper = true)
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "teacher")
public class Teacher extends Person {
public enum Course {
MATH, PHYSICS, CHEMISTRY, MUSIC, DRAW
}
public Teacher(String name, Course course) {
super(name);
this.course = course;
}
@Enumerated(EnumType.STRING)
private Course course;
}
生成的表结构:
CREATE TABLE `teacher` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`course` enum('CHEMISTRY','DRAW','MATH','MUSIC','PHYSICS') DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_5syf9tb34xn2g3cmjekoybhet` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `student` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`average_score` int NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_7pb8owoegbhhcrpopw4o1ykcr` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
通过这种方式可以将所有的子类都映射到同一张表:
// ...
@Entity(name = "Person2")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Table(name = "person2")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ...
}
@Entity(name = "Student2")
public class Student extends Person {
// ...
}
@Entity(name = "Teacher2")
public class Teacher extends Person{
// ...
}
生成的表结构:
CREATE TABLE `person2` (
`dtype` varchar(31) NOT NULL,
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`average_score` int DEFAULT NULL,
`course` tinyint DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
这里的dtype
字段是 Hibernate 自动生成的,用于区分不同实体的数据,比如下图的示例数据:
可以看到,如果不是当前类的属性,就用null
填充(相应的字段也不会存在NOT NULL
约束)。此外,父类(超类)的实例也可以被持久化(保存到数据库)。
用于区分同一张表中的不同实体数据的功能被称作鉴别器(Discriminator),我们可以指定鉴别器对应的表字段的名称、类型,以及不同实体对应的字段值。
@DiscriminatorColumn(name = "type",
discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorValue("null")
public class Person {
// ...
}
@DiscriminatorValue("1")
public class Student extends Person {
// ...
}
@DiscriminatorValue("2")
public class Teacher extends Person {
// ...
}
生成的表结构:
CREATE TABLE `person3` (
`type` int DEFAULT NULL,
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`average_score` int DEFAULT NULL,
`course` tinyint DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
现在区分实体数据的字段是type
,类型是int
。
@DiscriminatorValue
有两个特殊的值:
@DiscriminatorValue("null")
,对应表中鉴别器字段为null
的数据,可以用于映射超类(根)实体。@DiscriminatorValue("not null")
,如果数据与任何@DiscriminatorValue
都不对应,就会映射到由这个注解标记的实体。可以用多张表连接的方式映射实体的继承关系。
这种方式更符合继承的语义和一般直觉,数据库模型可以表示为:
person
表的主键id
同时是tercher
和student
表的外键,且都是一对一的对应关系。
JPA 中的实体表示:
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {
// ...
}
public class Student extends Person {
// ...
}
public class Teacher extends Person {
// ...
}
生成的表结构:
CREATE TABLE `person4` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `student4` (
`average_score` int NOT NULL,
`id` bigint NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `FKmbyfbqlr0ebtwwrdxgvguwncj` FOREIGN KEY (`id`) REFERENCES `person4` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `teacher4` (
`course` tinyint DEFAULT NULL,
`id` bigint NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `FKbuvkvpo0bh33c9tcjt7t5oyej` FOREIGN KEY (`id`) REFERENCES `person4` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
默认情况下子类对应的表用id
字段作为主键,可以通过@PrimaryKeyJoinColumn
注解进行定义:
@PrimaryKeyJoinColumn(name = "person_id")
public class Student extends Person {
// ...
}
@PrimaryKeyJoinColumn(name = "person_id")
public class Teacher extends Person {
// ...
}
生成的表结构:
CREATE TABLE `student4` (
`average_score` int NOT NULL,
`person_id` bigint NOT NULL,
PRIMARY KEY (`person_id`),
CONSTRAINT `FK67c9opl6rxlof46m2wpni45q1` FOREIGN KEY (`person_id`) REFERENCES `person4` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `teacher4` (
`course` enum('CHEMISTRY','DRAW','MATH','MUSIC','PHYSICS') NOT NULL,
`person_id` bigint NOT NULL,
PRIMARY KEY (`person_id`),
CONSTRAINT `FK2gyqqosqu69ld6t43aiiuyw1a` FOREIGN KEY (`person_id`) REFERENCES `person4` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
这种策略下,每个类都会对应一张表,并包含全部的属性。与@MappedSuperclass
不同的是,父类也会对应一张表。
@Entity(name = "Person5")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Person {
@Id
private Long id;
// ...
}
@Entity(name = "Student5")
public class Student extends Person {
// ...
}
@Entity(name = "Teacher5")
public class Teacher extends Person {
// ...
}
在这种情况下,不能依赖 Hibernate 的 identity key generation 生成主键值,而是要自己定义生成策略:
@Component
public class IdGenerator {
private long id = 0;
@Synchronized
long nextValue() {
id++;
return id;
}
}
@TestConfiguration
public class ExampleDataConfig {
@Autowired
private IdGenerator idGenerator;
@Bean
Student student() {
Student icexmoon = new Student("icexmoon", 90);
icexmoon.setId(idGenerator.nextValue());
return icexmoon;
}
// ...
}
生成的表数据类似下面这样:
用@Inheritance
注解定义的实体继承映射可以用多态查询 JPQL:
List<Person> persons = session.createQuery("from Person5", Person.class).getResultList();
persons.forEach(p->{
System.out.println(p);
});
对超类的查询结果会包含所有的子类:
Person(id=7, name=Tom)
Person(id=8, name=Adam)
Student(super=Person(id=1, name=icexmoon), averageScore=90)
Student(super=Person(id=2, name=lalala), averageScore=95)
Student(super=Person(id=3, name=JackChen), averageScore=85)
Teacher(super=Person(id=4, name=Catherine), course=MATH)
Teacher(super=Person(id=5, name=Tina), course=MUSIC)
Teacher(super=Person(id=6, name=LiLei), course=CHEMISTRY)
用@MappedSuperclass
定义的继承映射在查询时有所不同:
List<Person> persons = session.createQuery("from com.example.ineritancemapping.v1.Person", Person.class).getResultList();
persons.forEach(p->{
System.out.println(p);
});
JPQL 中的基类使用了完全限定名称(包含了完整包名),这是因为Person
本身并不是一个 Hibernate 管理的实体类。
查询结果同样包含所有的子类,当然并不包括超类本身,因为这种情况下超类不是实体类,没有持久化数据。
Student(super=Person(id=10, name=icexmoon), averageScore=90)
Student(super=Person(id=11, name=lalala), averageScore=85)
Student(super=Person(id=12, name=JackChen), averageScore=95)
Teacher(super=Person(id=10, name=BrusLee), course=CHEMISTRY)
Teacher(super=Person(id=11, name=Tina), course=MATH)
Teacher(super=Person(id=12, name=Cacherine), course=MUSIC)
还需要注意的是,此种情况下子类的 id 并不存在关联关系,由各自的 identity generator 生成,所以在上面这个示例中是可以出现重复 id 的。这和@Inheritance
实现的继承映射有所不同。
如果不希望某个子类出现在对父类的 JPQL 查询结果中,可以使用@Polymorphism(type = PolymorphismType.EXPLICIT)
标记。
The End,谢谢阅读。
可以从这里获取本文的完整示例代码。