Hibernate 双向多对多关联映射

1. 前言

通过本节课程的学习,你将发现关联对象之间的微妙关系。相信这种关系对你更深入地认识 HIbernate 有很大的帮助。

通过本节课程,你将了解到:

  • 多对多双向关联映射中哪一方是关系维系者;
  • 级联操作与关系维系者。

2. 关系维系者

新学期开始了,同学们选择了各自喜欢的课程,现在为学生添加选修课程的任务就要落在 Hibernate 的身上。一起来看看 Hibernate 是如何完成这个任务。

行动之前,先假设一个需求:

Hibernate” 同学觉得自己选修的课程太难了,现在想重新选择。

图片描述

重新选修之前,先删除原来选修的内容:

执行流程分析:

  1. 进入学生表,查询到 “Hibernate” 同学的信息;
  2. 删除 “Hibernate” 同学的所有选修课程。

2.1 解除级联对象之间的关系

删除方式有两种:

第一种解除方案

HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
		hibernateTemplate.template(new Notify<Student>() {
			@Override
			public Student action(Session session) {
				// 查询学生
				Student student =(Student)session.get(Student.class, new Integer(1));		
				// 查询Java课程
				Course javaCourse = (Course) session.get(Course.class, new Integer(1));
				// 查询C课程
				Course ccourse = (Course) session.get(Course.class, new Integer(2));
				// 解除关系
				student.getCourses().remove(javaCourse);
				student.getCourses().remove(ccourse);
				return null;
			}
		});

查询到 ”Hibernate“ 同学的信息,注意,此时 student 对象处于持久化状态,意味着 student 对象在程序世界的行为可以同步到数据库中。

查询 ”Hibernate“ 同学选修的 JavaC 两门课程,此时保存这两个课程信息的对象也处于持久化状态。

使用如下代码解除学生和课程之间的关系:

student.getCourses().remove(javaCourse);
student.getCourses().remove(ccourse);

因为学生对象、课程对象都处于持久化状态。它们在程序世界中的一言一行都会同步到数据库中。

既然在程序世界解除了彼此之间的关系,在数据库中,中间表中的关系描述数据也会自动删除。

从控制台上所显示出来的 SQL 语句其实也知道删除已经成功,这个就不贴出来了。

进入 MySql 验证一下:

图片描述

中间表中已经不存在和 ”Hibernate“ 同学相关的课程信息。

但是,此时你可能会有一个想法,刚刚是以学生对象为主动方,向课程对象提出了分手,那么,能不能以课程方为主动方提出分手呢?

试一下便知,测试之前,先恢复原来的内容:

图片描述

执行下面的实例代码:

HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
		hibernateTemplate.template(new Notify<Student>() {
			@Override
			public Student action(Session session) {
				// Hiberante学生
				Student student =(Student)session.get(Student.class, new Integer(1));		
				// 查询Java
				Course javaCourse = (Course) session.get(Course.class, new Integer(1));
				// 查询C
				Course ccourse = (Course) session.get(Course.class, new Integer(2));
				// 解除关系,以课程对象为主动方
				javaCourse.getStudents().remove(student);
				ccourse.getStudents().remove(student);			
				return null;
			}
		});

可能会让你失望,这次操作对数据库没有任何影响。

可见,分手只能是由学生对象提出来。

虽然在前面课程中,咱们配置了学生类和课程类的双向多对多关联映射,但是,两者之间只能有一个主动方,这里要了解,所谓主动方,就是关系的维系者。

关系的建立和解除只能由主动方提供。是不是有点像霸道总裁的狗血故事。

第二种解除方案

Ok!一起继续了解第 2 种方案。

不管是哪种方案,切记,只能是由主动方提出分手。
行事之前,一定要先检查数据是否存在。

HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
	hibernateTemplate.template(new Notify<Student>() {
		@Override
		public Student action(Session session) {
			// Hibernate学生
			Student student =(Student)session.get(Student.class, new Integer(1));		
           //解除关系
			student.getCourses().removeAll(student.getCourses());
			return null;
		}
	});

和第一种方案相比较,不再查询课程信息,由学生对象单方面一次性解除关系。

student.getCourses().removeAll(student.getCourses());
				return null;

执行结果没有什么意外,程序世界中关系的解除操作同步到了数据库中。

一起看看控制台上输出的信息:

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: 
    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=?
Hibernate: 
    delete 
    from
        score 
    where
        stuId=?

大家可以看到,Hibernate 接收到解除操作后,立即由中间表连接到课程表,把相关课程信息从中间表中抺出。

一切进行得简单而有序:

  • 记住,持久化对象的行为可以同步到数据库中去;
  • 多对多双向关联映射中,有主动方和被动方一说。

2.2 建立级联对象之间的关系

好!现在为 ”Hibernate“ 同学选修新的课程。比如说,想选择 DB 课程和 JAVA 课程。

天呀,刚刚不是才解除了 JAVA 课程吗。你管得着吗,咱们就是这么任性,想解除就解除,想建立就建立 ,谁叫 Hibernate 这么方便呢。

有了前面的知识,应该难不倒我们了。

现在来看看 Hibernate 实例:

HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
		hibernateTemplate.template(new Notify<Student>() {
			@Override
			public Student action(Session session) {
				// 添加新学生
				Student student =(Student)session.get(Student.class, new Integer(1));		
				// 查询Java
				Course javaCourse = (Course) session.get(Course.class, new Integer(1));
				// 查询C
				Course dbCourse = (Course) session.get(Course.class, new Integer(3));
				// 确定关系
				student.getCourses().add(javaCourse);
				student.getCourses().add(dbCourse);
				return null;
			}
		});

关系还是在程序中直接体现,并且是由学生对象维护。
可以进入数据库,查看一下:

图片描述

还是再叮嘱一下,关系只能由学生方维护的。

此处,应该有疑问,为什么只能是学生方,而不能是课程方,难道在 Java 的世界里也有命运一说。

这个命运的安排是由开发者来决定的,在进行关联注解时,那一方使用了 mappedBy 属性,则这一方就是被动方,很好理解,这个属性本身的含义就是说,听对方的。

所以说,谁是霸道总裁,看开发者的心情。理论上讲,多对多中,两者应该是平等关系。

刚刚的解除和重新建立都是对已经存在的学生进行的。

如果班里转来了一名叫 ”HibernateTemplate“ 的新学生,他想选修 JAVADB,则应该怎么操作呢?

很简单啊!

  • 学生没有,添加就是;
  • 课程信息有,从表中查询就是;
  • 关系不存在,建立就是。

下面便是如你所想的实例代码:

HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
	@Override
	public Student action(Session session) {
		// 添加新学生
		Student student = new Student("HibernateTemplate", "男");
		// 查询Java课程
		Course javaCourse = (Course) session.get(Course.class, new Integer(1));
		System.out.println(javaCourse.getCourseName());
	    // 查询DB课程
		Course dbCourse = (Course) session.get(Course.class, new Integer(3));
		System.out.println(dbCourse.getCourseName());
	    // 确定关系
	    student.getCourses().add(javaCourse);
		student.getCourses().add(dbCourse);
		return null;
	}
});

是的,这个代码本身没有问题。

但是,这里会有一个小坑,这个坑没有填的话,你可能会测试不成功。

学生类中,一定要记住对下面的属性进行实例化:

private Set<Course> courses=new HashSet<Course>();

否则就会有空指针异常抛出来。前面没有,是因为数据库中有数据,Hibernate 帮咱们自动实例化了。

但现在是一个新学生,Hiberante 可不会帮你实例化他的课程集合属性。

也就是不能任何时候都依靠 Hibernate,它也有顾全不到的地方。为了让你宽心,还是看一下数据库中数据吧:

图片描述

3. 级联删除

前面讲解双向一对多的时候,也提到了级联删除。最大的印象就是,如果双方都打开了级联删除,删除时就如同推倒了多米诺骨牌的第一张牌,整个数据链都会删除。

多对多关联一对多关联多了一张中间表,在进行级联删除的时候,到底会发生什么事情?

在此也有必要拿出来说一说。

为了不让事情的发展如山崩一样不可控制,先打开学生类的级联操作功能:

private Set<Course> courses=new HashSet<Course>();
	@ManyToMany(targetEntity = Course.class,cascade=CascadeType.ALL)
	@JoinTable(name = "score", joinColumns = @JoinColumn(name = "stuId", referencedColumnName = "stuId"), 
	inverseJoinColumns = @JoinColumn(name = "courseId", referencedColumnName = "courseId"))
	public Set<Course> getCourses() {
		return courses;
	}

这里使用 CascadeType.ALL

来一段测试实例,删除刚才添加的 HibernateTemplate 同学。

他会说我好悲惨,才进来没有多久。

HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
		hibernateTemplate.template(new Notify<Student>() {
			@Override
			public Student action(Session session) {
				// 查询学生
				Student student =(Student)session.get(Student.class, new Integer(23));
				session.delete(student);
                return null;
			}
		});

无惊无喜,一切按照预先的设想进行。
图片描述

删除学生时,中间表中与此学生有关联的信息,也就是说此学生选修的课程信息也自动被删除了。

但是,会有一个想法,如果删除课程,则中间表中记录的与此课程有关的信息是否会自动删除呢?

OK!开始行动之前,可别忘记在课程类中打开级联操作选项:

嘿嘿!现在两边的级联操作功能都已经打开。

private Set<Student> students=new HashSet<Student>();
	@ManyToMany(targetEntity = Student.class, mappedBy = "courses",cascade=CascadeType.ALL)
	public Set<Student> getStudents() {
		return students;
	}

打开后,执行删除 C 课程的实例,谁让 C 不好学了。

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(2));
				session.delete(course);
                
				return null;
			}
		});

这只是一个很简单的代码,但是却发生如雪崩一样的事件。

到底发生了什么事情?

大家进入 MySql 看看就知道了。

图片描述
3张表中空空如也,所有数据都没有了。

就如同前面讲解一对多的级联删除一样。同样适用于多对多关联映射之中。

因两边都已经打开了级联,删除操作如同无法控制的坏情绪,删除课程时,以中间表为连接,反复来往于三张表,把相关信息全部删除。

所以,使用级联时一定要小心,否则,小心脏真的有点受不了。

4. 小结

好了,本节课可以结束了,通过本节课,大家了知道无论是双向一对多,还是双向多对多,都会有一个主动方。
在双向多对关联中,主动方有解除关系能力。

级联操作功能用得好,能一劳永逸。但是,如果没有用好,则会如同打开了潘多拉魔盒,发生雪崩一样的灾难。