Hibernate 多对多关联映射
1. 前言
本节课,咱们一起继续聊聊多对多关联映射。通过本节课程,你将了解到:
- 多对多关联映射的实现;
- 双向多对多关联映射的实现。
2. 多对多关联映射
首先了解表中的多对多关系,学生表中的数据和课程表中的数据就存在多对多关系。
一名学生可以选修多门课程,一门课程可以供多名学生选修。
数据库通过主外键的机制描述表中的数据之间的关系。对于存在多对多关系的数据表,借助于中间表,分拆成两个一对多(或者多对一)。
中间表的出现,完美地表述了学生数据和课程数据之间的多对多关系。
数据库的世界中有学生表、课程表,自然,Java 程序中就会有学生实体类、课程实体类。
不对,好像遗漏了什么!
别忘了,表是有 3 张的(不是还有中间表吗)。那么 Java 程序中的实体类是不是应该也要有 3 个:
- 学生表对应的实体类;
- 班级表对应的实体类;
- 中间表对应的实体类。
至于中间表所对应的实体类是否应该有:答案是可以有、也可以没有。
如果中间表仅仅只是记载了学生和课程的关系,中间表的角色定位只是一个桥梁。这种情况下,Java 程序中可以不描述中间表结构。
Java 程序中的实体类不仅仅是用来模仿表结构,更多是看上了表中的数据。
如果中间表除了连接作用,还保存了程序中需要的数据,则 Java 程序需要一个实体类填充数据。如:
针对这 2 种情况,实体类之间的映射关系会有微妙的变化。
3. 没有中间表实体类的映射
如果中间表仅仅只是充当桥梁作用,没有程序需要的实质性数据时,程序中可以没有中间表对应的实体类。
学生和课程的关系,直接在学生类和课程类中体现彼此关系就可以:
新建课程实体类:
@Entity
public class Course {
private Integer courseId;
private String courseName;
private String courseDesc;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getCourseId() {
return courseId;
}
//省略其它代码
}
因为一名学生对应多门课程,在学生实体类中添加集合属性:
private Set<Course> courses;
完成程序级别上的关系描述后,还需告诉 Hibernate,实体类中的集合属性数据来自哪一张以及如何获取?
为了把问题简单化,在学生实体类中只体现和课程的关系 ,前面映射内容注释或删除。
学生实体类的完整描述:
private Set<Course> courses;
@ManyToMany(targetEntity = Course.class)
@JoinTable(name = "score", joinColumns = @JoinColumn(name = "stuId", referencedColumnName = "stuId"),
inverseJoinColumns = @JoinColumn(name = "courseId", referencedColumnName = "courseId"))
public Set<Course> getCourses() {
return courses;
}
- @ManyToMany 告诉 Hibernate, course 集合中的数据来自课程表 ;
- @JoinTable 告诉 Hibernate 获取课程表中数据时需要借助 score 中间表。分别描述中间表和学生表、课程表的连接字段。
在 Hibernate 主配置文件中修改或添加如下信息:
<property name="hbm2ddl.auto">create</property>
<mapping class="com.mk.po.Student" />
<mapping class="com.mk.po.Course" />
执行下面的测试实例:
@Test
public void testGetStuAndCourse() {
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
}
查看 MySql 中的表:
切记把下面的信息修改回来:
<property name="hbm2ddl.auto">update</property>
手工添加测试数据:
好!通过测试实例见证 Hibernate 的神奇。
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
Student stu=(Student)session.get(Student.class, new Integer(1));
System.out.println("---------------------------");
System.out.println("学生姓名:"+stu.getStuName());
System.out.println("----------------------------");
System.out.println("学生选修课程数:"+stu.getCourses().size());
return stu;
}
});
查看控制台上面的输出结果:
Hibernate:
select
student0_.stuId as stuId1_1_0_,
student0_.stuName as stuName2_1_0_,
student0_.stuPassword as stuPassw3_1_0_,
student0_.stuPic as stuPic4_1_0_,
student0_.stuSex as stuSex5_1_0_
from
Student student0_
where
student0_.stuId=?
---------------------------
学生姓名:Hibernate
----------------------------
Hibernate:
select
courses0_.stuId as stuId1_1_1_,
courses0_.courseId as courseId2_2_1_,
course1_.courseId as courseId1_0_0_,
course1_.courseDesc as courseDe2_0_0_,
course1_.courseName as courseNa3_0_0_
from
score courses0_
inner join
Course course1_
on courses0_.courseId=course1_.courseId
where
courses0_.stuId=?
学生选修课程数:2
Hibernate 构建了两条 SQL 语句,先是查询到学生信息,需要课程信息时,再通过中间表连接到课程表,查询出课程相关信息。
可得出结论:默认情况下,Hibernate 使用了延迟加载。如此做,Hibernate 是考虑了性能的。
4. 考虑中间表的映射
命名为 score 的中间表除了维系学生表和课程表的关系,还存储有学生成绩。如果程序中需要成绩数据,则需要创建一个成绩实体类。
现在就有了学生、课程、成绩 3 个 实体类。本质是映射成两个多对一(或一对多)的关系:
- 学生实体类和成绩实体类一对多;
- 课程实体类和成绩实体类的一对多:
具体的代码就不再贴出,大家可参考一对多的课程内容。
本节课不关心学生的课程成绩是多少,只关心,如何通过学生找到课程,或通过课程找到学生。
有一个地方需要注意:
默认情况下,中间表使用课程 ID 和学生 ID 联合做主键,也可以提供一个自定义的主键。
5. 双向多对多映射
前面实现了学生查询到课程,如何在查询课程时,查询到学生信息。很简单,在课程 PO 中,添加学生集合属性:
// 学生信息
private Set<Student> students;
同样使用 @ManyToMany 注解告诉 Hibernate 数据源头及查询方法:
private Set<Student> students;
@ManyToMany(targetEntity = Student.class, mappedBy = "courses")
public Set<Student> getStudents() {
return students;
}
执行下面的测试实例:
HibernateTemplate<Course> hibernateTemplate = new HibernateTemplate<Course>();
hibernateTemplate.template(new Notify<Course>() {
@Override
public Course action(Session session) {
Course course=(Course)session.get(Course.class, new Integer(1));
System.out.println("---------------------------");
System.out.println("课程名称:"+course.getCourseName());
System.out.println("----------------------------");
System.out.println("选修此课程的学生数:"+course.getStudents().size());
return course;
}
});
控制台输出结果:
Hibernate:
select
course0_.courseId as courseId1_0_0_,
course0_.courseDesc as courseDe2_0_0_,
course0_.courseName as courseNa3_0_0_
from
Course course0_
where
course0_.courseId=?
---------------------------
课程名称:java
----------------------------
Hibernate:
select
students0_.courseId as courseId2_0_1_,
students0_.stuId as stuId1_2_1_,
student1_.stuId as stuId1_1_0_,
student1_.stuName as stuName2_1_0_,
student1_.stuPassword as stuPassw3_1_0_,
student1_.stuPic as stuPic4_1_0_,
student1_.stuSex as stuSex5_1_0_
from
score students0_
inner join
Student student1_
on students0_.stuId=student1_.stuId
where
students0_.courseId=?
选修此课程的学生数:2
同样,Hibernate 采用的是延迟加载模式。先查询课程信息,当开发者需要学生信息时,才构建一条利用中间表进入学生表的 SQL 查询到学生信息。
可通过学生表查询到课程表 ,也能从课程表查询到学生表,这种多对多关联映射称为双向映射关联。
6. 小结
本节课和大家一起聊了聊多对多映射的实现。多对多是一种常见的关系。
多对多的映射实现可以有 2 种方案。使用中间表映射,或不使用中间表映射。
下一节课,继续讲解多对多映射中的添加、更新级联操作,相信会给你带来更多的惊喜。