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

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;
	}

很简单,只需要使用 @OneToManycascade 属性,就能让 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. 小结

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

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

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

相关推荐
王码码20358 小时前
Go语言的测试:从单元测试到集成测试
后端·golang·go·接口
王码码20358 小时前
Go语言中的测试:从单元测试到集成测试
后端·golang·go·接口
嵌入式×边缘AI:打怪升级日志9 小时前
使用JsonRPC实现前后台
前端·后端
小码哥_常10 小时前
从0到1:Spring Boot 中WebSocket实战揭秘,开启实时通信新时代
后端
lolo大魔王10 小时前
Go语言的异常处理
开发语言·后端·golang
IT_陈寒12 小时前
Python多进程共享变量那个坑,我差点没爬出来
前端·人工智能·后端
码事漫谈12 小时前
2026软考高级·系统架构设计师备考指南
后端
AI茶水间管理员13 小时前
如何让LLM稳定输出 JSON 格式结果?
前端·人工智能·后端
其实是白羊14 小时前
我用 Vibe Coding 搓了一个 IDEA 插件,复制URI 再也不用手动拼了
后端·intellij idea
用户83562907805114 小时前
Python 操作 Word 文档节与页面设置
后端·python