介绍JPA的Many-To-Many 关系

介绍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对关系型数据库中的多对多关系进行建模。共有三种方式,各有优劣:

  • 代码清晰
  • 数据库清晰
  • 能够给关系附加属性
  • 关系可以连接多少个实体,相同实体能支持多个连接
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值