Skip to main content
 首页 » 编程设计

介绍JPA的Many-To-Many 关系

2022年07月19日139jillzhang

介绍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
阅读延展