14. Hibernate 一对多双向关联映射

avatar
作者
猴君
阅读量:0

1. 前言

本节课程和大家一起聊聊一对多关联映射。通过本节课程,你将了解到:

  • 如何实现一对多关联映射;

  • 如何实现双向一对多关联映射;

  • 关联映射中的级联操作。

2. 一对多关联映射

关系型数据库中表与表中的数据存在一对多(或多对一)关系。

如学生表、班级表。一个班级有多个学生,多个学生可以在同一个班级。

一对多或多对一本质上是一样的,如同一块硬币的正面和反面,只是看待事物的角度不同而已。

数据库中有学生表、班级表。使用 Hibernate 进行数据操作时, 程序中就应该有学生类、班级类。

同时学生类、班级类应该使用 OOP 语法描述出如同学生表和班级表一样的关系。并且还要让 Hibernate 看得懂。

有了前面的基础,直接上代码:

创建班级类:

@Entity public class ClassRoom { 	private Integer classRoomId; 	private String classRoomName; 	private String classRoomDesc; 	@Id 	@GeneratedValue(strategy = GenerationType.IDENTITY) 	public Integer getClassRoomId() { 		return classRoomId; 	} 

需求:查询学生时,得到学生所在班级信息。

进入学生类,添加如下代码,描述学生类班级类的关系:

private ClassRoom classRoom; 

除此之外,还需要让 Hibernate 知道,这个对象属性的值来自于班级表中的对应数据,进一步修改代码:

private ClassRoom classRoom; @ManyToOne(targetEntity=ClassRoom.class) @JoinColumn(name="classRoomId") public ClassRoom getClassRoom() { 	return classRoom; } 
  • @ManyToOne 告诉 Hibernate,从学生的角度来看,学生是多的一边,查询班级表可以得到学生所在班级信息。
  • @JoinColumn 告诉 Hibernate 需要带着指定的字段值到班级表中匹配数据。

修改 Hibernate 主配置文件中内容:

<property name="hbm2ddl.auto">create</property> <mapping class="com.mk.po.Student" /> <mapping class="com.mk.po.ClassRoom" /> 

为了让事情变得简单明了,在主配置文件中只保留学生类班级类的映射关系。

学生类中的所有属性描述:

private Integer stuId; 	private String stuName; 	private String stuSex; 	private String stuPassword; 	private Blob stuPic; 	private ClassRoom classRoom;     @ManyToOne(targetEntity=ClassRoom.class)     @JoinColumn(name="classRoomId") 	public ClassRoom getClassRoom() { 		return classRoom; 	} 

使用上一节课的模板对象跑一个空测试实例:

@Test public void testGetByTemplate() { 	HibernateTemplate<Student> hibernateTemplate=new HibernateTemplate<Student>();	 } 

目的是让 Hibernate 重新创建学生表、班级表。别忘记啦,自动创建表后,修改回:

<property name="hbm2ddl.auto">update</property> 

进入 MySql,在学生表、班级表中手工添加几条测试数据:

到了完成需求的时候,测试下面实例:

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("学生姓名:"+stu.getStuName()); 				System.out.println("学生所在班级:"+stu.getClassRoom().getClassRoomName()); 				return stu_; 			} 		}); 

控制台输出结果:

Hibernate:      select         student0_.stuId as stuId1_1_1_,         student0_.classRoomId as classRoo6_1_1_,         student0_.stuName as stuName2_1_1_,         student0_.stuPassword as stuPassw3_1_1_,         student0_.stuPic as stuPic4_1_1_,         student0_.stuSex as stuSex5_1_1_,         classroom1_.classRoomId as classRoo1_0_0_,         classroom1_.classRoomDesc as classRoo2_0_0_,         classroom1_.classRoomName as classRoo3_0_0_      from         Student student0_      left outer join         ClassRoom classroom1_              on student0_.classRoomId=classroom1_.classRoomId      where         student0_.stuId=? 学生姓名:Hibernate 学生所在班级:c1911 

Hibernate 使用 left outer join 构建了一条多表查询语句!

3. 双向一对多关联映射

需求:查询班级时,想知道班上有多少名学生,又应该如何映射?

进入班级类,添加如下属性:

private Set<Student> students; 

使用集合属性 students,描述了一个班有多名学生。

为什么使用 Set 集合?

因为一个班级内不可能出现两个完全相同的学生对象。

这还仅仅只是 OOP 层面上的关系,还需要告诉 Hibernate 应该如何填充数据。

添加下面代码:

private Set<Student> students; @OneToMany(targetEntity=Student.class,mappedBy="classRoom") public Set<Student> getStudents() { 	return students; } 
  • @OneToMany:很直白的说明了一个班级会有多名学生,指引 Hibernate 在填充数据时,要找到所有学生,别遗漏了;
  • 属性 mappedBy=“classRoom”: 告诉 Hibernate,班级和学生之间的关系在学生类中已经说的够明白了,应该不需要再废话了吧。

OK!把前面的测试实例改为查询班级信息:

HibernateTemplate<ClassRoom> hibernateTemplate = new HibernateTemplate<ClassRoom>(); 		 hibernateTemplate.template(new Notify<ClassRoom>() { 			@Override 			public ClassRoom action(Session session) { 				ClassRoom classRoom=(ClassRoom)session.get(ClassRoom.class, new Integer(1)); 				System.out.println("班级名称:"+classRoom.getClassRoomName()); System.out.println("------我是分隔线------------------------"); 				System.out.println("班级学生人数:"+classRoom.getStudents().size()); 				return classRoom; 			} 		}); 

查看控制台输出结果:

Hibernate:      select         classroom0_.classRoomId as classRoo1_0_0_,         classroom0_.classRoomDesc as classRoo2_0_0_,         classroom0_.classRoomName as classRoo3_0_0_      from         ClassRoom classroom0_      where         classroom0_.classRoomId=? 班级名称:c1911 ------我是分隔线------------------------ Hibernate:      select         students0_.classRoomId as classRoo6_0_1_,         students0_.stuId as stuId1_1_1_,         students0_.stuId as stuId1_1_0_,         students0_.classRoomId as classRoo6_1_0_,         students0_.stuName as stuName2_1_0_,         students0_.stuPassword as stuPassw3_1_0_,         students0_.stuPic as stuPic4_1_0_,         students0_.stuSex as stuSex5_1_0_      from         Student students0_      where         students0_.classRoomId=? 班级学生人数:2 

会发现一个很有意思的地方。Hibernate 查询班级时,构建了两条 SQL

先查询班级,当需要学生信息时,才构建查询学生的 SQL

大家应该也猜出来了,当从学生表查询班级表时,Hibernate 采用的是立即策略。

当查询从班级表查询到学生表时,Hibernate 采用的是延迟加载策略。

采用延迟加载都只有一个目的,需要时加载,提高响应速度。

现在,学生类和班级类的映射配置信息,能让 Hibernate 自动从学生表查询到班级表,也能从班级表查询到学生表。

这种 2 个实体类中的映射关系就称为双向一对多关联映射

无论是 @ManyToOne 还是 @OneToMany 注解都有 fetch 属性,可以设置的值有 2 个选择:

  • FetchType.EAGER
  • FetchType.LAZY

所以,在双向一对多关联映射可以选择是否启用延迟加载,这和一对一关联映射中是一样的,就不在此重复复述。

是否采用延迟加载,由项目逻辑决定。

4. 一对多关联映射中的级联操作

什么是级联操作?

关系型数据库中由主外键维系的两张表,具有主从关系。

如学生表和班级表,班级班是主表,学生表是从表。

类似于删除某一个班级的信息,则需要先删除所在班的学生信息,再删除班级信息,这个操作就是级联操作。

所谓级联操作,指操作一张表时,是否会牵连到与之有关联的其它表。

现在,咱们是使用 Hibernate 进行数据操作,不可能还要劳驾自己亲力亲为吧。只需要做些简单配置,就可以让 Hibernate 自动做级联操作。

进入班级类,修改代码如下:

@OneToMany(targetEntity=Student.class,mappedBy="classRoom",cascade=CascadeType.REMOVE) 	public Set<Student> getStudents() { 		return students; 	} 

很简单,只需要使用 @OneToMany 的 cascade 属性,就能让 Hibernate 明白如何做级联操作。默认情况下,没有级联效应。

cascade 是一个枚举类型:

public enum CascadeType {     ALL,     PERSIST,     MERGE,     REMOVE, 	REFRESH,     DETACH } 
  • ALL: 级联所有操作;
  • PERSIST: 级联新增;
  • MERGE: 级联更新或者新增;
  • REMOVE: 级联删除;
  • REFRESH: 级联刷新;
  • DETACH: 级联分离。

测试删除班级实例:

HibernateTemplate<ClassRoom> hibernateTemplate = new HibernateTemplate<ClassRoom>(); 		 hibernateTemplate.template(new Notify<ClassRoom>() { 			@Override 			public ClassRoom action(Session session) { 				ClassRoom classRoom=(ClassRoom)session.get(ClassRoom.class, new Integer(1));	 				 session.delete(classRoom); 				return null; 			} 		}); 

如果不添加 cascade 相关说明,因为有学生引用班级信息,班级信息是不能被删除的。

添加后再测试,查看表中内容:班级以及班级所在学生信息全部删除!

 

删除班级时能级联删除学生,反过来,删除学生能删除班级吗?

想法很好,实践是检验真理的唯一手段,学生类中修改成如下代码:

@ManyToOne(targetEntity=ClassRoom.class,cascade=CascadeType.REMOVE)     @JoinColumn(name="classRoomId") 	public ClassRoom getClassRoom() { 		return classRoom; 	} 

测试实例:

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

结果很残酷!学生被删除了,班级也被删除了!

级联级联,只要设置了级联,不管删除学生还是班级,只要在对应表中有引用关系的数据就会被删除。

现在,学生类、班级类中的级联删除都打开了。如果对下面情形的数据(编号 1、2 的学生的班级编号都为 1)进行删除操作,则会发生什么事情?

 

数据库中的数据如下:

 测试删除编号为 1 的学生:

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

进入 MySql,查看一下:

天呀!这是级联还是株连呀,太让人后怕,数据都没有了。

删除学生时,会级联删除和学生有关的班级,班级删除时,又会查看学生表中是否还存在与班级有关联的学生,有,则一刀下去,连根拔起。

Hibernate 有点刹不住车,产生了级联连锁反应。

针对上面的测试,如果班级表的级联关闭,执行测试代码,请问结果又会怎样?

本节课程,讲解了级联删除,级联添加的内容留到下节课继续展开。

5. 小结

本文和大家聊了双向一对多关联映射。

无论是一对一双向关联映射,还是一对多双向关联映射。都可以根据需要随时设置是否延迟加载、级联等操作。

在使用级联操作时,一定要小心,避免产生连锁反应,删除了不应该删除的数据。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!