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 告诉 Hibernatecourse 集合中的数据来自课程表 ;
  • @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 种方案。使用中间表映射,或不使用中间表映射。

下一节课,继续讲解多对多映射中的添加、更新级联操作,相信会给你带来更多的惊喜。