介绍JPA的Many-To-Many 关系
本文我们讨论JPA中多种方式处理多对多关系。
为了方便阐述,使用大家熟悉的场景,学生、课程以及两者之间不同的关系。同时示例代码也不用过多的属性,仅展示核心的配置。
1. 多对多基础
1.1. 数据库建模
关系是两个类型实体之间的连接。在多对多情况下,两边都能关联多个实例。
注意,实体类型可能与其自身存在关系。例如当建模家谱时,每个节点都是一个人实例,如果讨论父子关系,两个参与者都是人类型。但是讨论单个类型还是多个类型之间关系并没有太大区别。由于考虑两个不同实体类型之间关系更容易,所以我们将用它来说明。
下图显示学生选修他们喜欢的课程,学生可以选修多个课程,多个学生可以喜欢相同的课程。
在RDBMS中需要使用外键创建关系。既然两边都能引用对方,因此需要创建关联表包括两边的外键,一般在关联表中设置两边主键作为联合主键。
1.2. 实现简单多对多模式
使用POJO表示多对多关系很容易,需要在两个类中包括对方实例的集合,之后在实体类上增加@Entity注解,并在主键上标记@Id注解。当然还需要配置两者之间的关系,因此需在集合上增加注解@ManyToMany:
@Entity
@Data
class Student {
@Id
Long id;
@ManyToMany
Set<Course> likedCourses;
// additional properties
}
@Entity
@Data
class Course {
@Id
Long id;
@ManyToMany
Set<Student> likes;
// additional properties
}
另外还需要配置RDBMS的关系模型。
所有者端是我们配置关系的地方,在本例中,我们将选择Student类。我们在Student类中使用@JoinTable注解定义数据库关系,@JoinColumn注解定义外键。joinColumns属性连接自己, inverseJoinColumn属性连接对方:
@ManyToMany
@JoinTable(
name = "course_like",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
Set<Course> likedCourses;
注意使用@JoinTable,甚至@JoinColumn不是必须的,如果没有指定JPA将生成表名和列名。但是,JPA的策略并不总是与我们使用的命名约定相匹配。因此一般建议配置表名和列名。
另一个实体仅需要提供关联表的名称,通过在@ManyToMany注解的mappedBy属性中设置。Course 类的配置如下:
@ManyToMany(mappedBy = "likedCourses")
Set<Student> likes;
注意,因为在数据库中的多对多关系并没有拥有者的概念。因此我们也可以配置关联表在Course类中,在Student中配置引用。
2. 实现组合多对多模式
2.1. 关系属性建模
假设让学生评价课程,学生可以评价多门课程,多个学生可以评价相同课程,这也是一种多对多关系。这稍微有点复杂,我们需要存储学生对课程的评价得分。
该存储在哪里呢?因为学生可以对不同课程进行评价,因此不能存储在Student实体中,同理也不能存储在Course中。好的方案是在关系本身增加额外属性。给关键增加额外属性的ER图如下:
与简单多对多建模相似,仅不同的是在关联表中增加额外属性:
2.2. 实现组合多对多模式
实现简单多对多模式很直接。因为实体是直接连接,现在问题是不能给关系增加属性。既然在JPA中能映射属性值数据库字段,我们需要为关系创建新的实体类。每个JPA实体需要主键,但我们主键是组合组件,因此需要创建新的类包括键的内容:
@Embeddable
class CourseRatingKey implements Serializable {
@Column(name = "student_id")
Long studentId;
@Column(name = "course_id")
Long courseId;
// standard constructors, getters, and setters
// hashcode and equals implementation
}
注意:组合键类需要符合下列几点条件:
- 必须标记@Embeddable注解
- 必须实现 java.io.Serializable接口
- 需要提供 hashcode() 和 equals() 方法的实现
- 没有字段可以是实体本身
2.3. 使用组合键
创建关联类,在其中使用组合键类,表示关联表:
@Entity
@Data
class CourseRating {
@EmbeddedId
CourseRatingKey id;
@ManyToOne
@MapsId("student_id")
@JoinColumn(name = "student_id")
Student student;
@ManyToOne
@MapsId("course_id")
@JoinColumn(name = "course_id")
Course course;
int rating;
}
上面代码与政策实体实现类似,但有些差异:
- 使用@EmbeddedId注解标记主键,其为CourseRatingKey类的实例。
- 使用@MapsId注解标记student 和 course 字段。
@MapsId注解表示这些字段是主键的组成部分,是多对一关系的外键。上面我们提醒过,组合键不能有实体。
下面我们开始在Student 和 Course 类中配置反向引用:
class Student {
// ...
@OneToMany(mappedBy = "student")
Set<CourseRating> ratings;
// ...
}
class Course {
// ...
@OneToMany(mappedBy = "course")
Set<CourseRating> ratings;
// ...
}
2.4. 深入讨论
我们使用@ManyToOne注解配置Student 和 Course 类。因为我们使用新的实体把多对多关系分解为两个多对一关系。
为什么我们能这样?我们仔细分析前面的示例,其中包括两个多对一关系,其实在RDBMS中没有多对多关系,我们称关联表为多对多关系,这时我们自己的建模需要。这时让我们更好理解,关联表仅为实现细节,并不需要真正关心它。
另外,该方案中还有一个特定没有提及。简单多对多模型在两个实体之间创建关系,因此不能扩展关系至更多实体。但组合多对多关系实现中没有限制,我们可以在任意数量实体类型键建立关系。
举例,当有多个教师教授一门课程时,学生可以针对每位教师教授课程进行评分。这样的话评价是三个实体之间的关系:学生、课程、教师。
3. 使用新实体
3.1. 关系属性建模
加入让学生注册课程,同时也需要记录学生学习课程的得分,因此需要存储学生学习课程的得分。
理想情况下可以采用前面的方案解决问题,创建关联类并使用组合键。然而实际情况下,学生可能不会一次通过,因此学生和课程的组合键会重复。因此前面的方案不能解决,我们需要单独主键。下面新增一个实体,包括学习相关属性:
这时注册实体表示其他两个实体之间的关系,因为其为独立实体,拥有自己的主键。
注意,在前面的解决方案中我们有一个复合主键,它是由两个外键创建的。现在,这两个外键不再是主键的一部分:
3.2. JPA实现
既然coure_registration 表是正常的表,我们需创建普通的实体:
@Entity
class CourseRegistration {
@Id
Long id;
@ManyToOne
@JoinColumn(name = "student_id")
Student student;
@ManyToOne
@JoinColumn(name = "course_id")
Course course;
LocalDateTime registeredAt;
int grade;
// additional properties
// standard constructors, getters, and setters
}
同时我们需要配置Student和Course类的关系:
class Student {
// ...
@OneToMany(mappedBy = "student")
Set<CourseRegistration> registrations;
// ...
}
class Course {
// ...
@OneToMany(mappedBy = "courses")
Set<CourseRegistration> registrations;
// ...
}
同样我们在前面配置了关系。因此我们只需要告诉JPA,它在哪里可以找到配置。
注意,我们可以使用这个解决方案来解决前面的问题:学生对课程进行评级。
但是除非必须创建专用主键,否则创建主键会感觉很奇怪。从RDBMS的角度来看这没有多大意义,因为将两个外键组合在一起就形成了一个完美的组合键。此外,这个组合键有一个明确的含义:我们在关系中连接哪些实体。
因此在这两种实现之间的选择通常不是很有必要。
4. 总结
本文我们讨论如何使用JPA对关系型数据库中的多对多关系进行建模。共有三种方式,各有优劣:
- 代码清晰
- 数据库清晰
- 能够给关系附加属性
- 关系可以连接多少个实体,相同实体能支持多个连接
本文参考链接:https://blog.csdn.net/neweastsun/article/details/103216107