Hibernate对象关系映射详解之“一对多”关系映射
之前学习Hibernate框架的时候,对这七大关系映射一直是云里雾里的,虽然可以仿照写出代码,但是不能独立编写出来。鉴于工作中这个知识点使用的几率还是非常大的,所以花了一点时间静下心来学习了一下,在这里写下一点学习笔记和大家交流。欢迎大家指点交流!(下面的笔记以及所有的示例都是使用注解)
我理解的关联映射就是将关联关系映射到数据库里,在对象模型中就是对一个或多个对象的引用。
七大关联映射有:单向一对一,单向一对多,单向多对一,单向多对多,双向一对一,双向一对多,双向多对多。
在学习关联映射之前,先理解何为“单向”?何为“双向”?
1、在使用注解实现关系映射时,只在其中一端进行配置则表示是单向关系映射,在两端同时进行配置则表示双向关系映射。
2、单向和双向的实质区别就是:哪一方负责维护该层关系。例如在单向关系中,配置的一端负责关系的维护,另外一端不负责;双向则是双方都要维护该层关系。加载负责维护关系的一端,系统会自动加载另一端。
举一个生活中的例子理解双向和单向:
情景假设:A 和 B 是两个人
(情景一)单向关系:
如果仅仅只是A喜欢B,则这层“喜欢”的关系就只是由A负责维护(或者说只能由A来维护),B不会进行维护。因为A对象中“喜欢”的属性中有B,所以在加载A这个人的数据时,同时会加载出B的相关数据;但是加载B的数据时,不会加载出A的相关数据,因为B的“喜欢”属性中没有A,甚至是没有“喜欢”这个属性。
(情景二)双向关系:
如果A 喜欢 B,同时B也喜欢A,则这层“喜欢”的关系就是由两个人同时维护(或者说可以由两个人维护)。同理于上,因为双方的“喜欢”属性中都有对方,所以在加载任意一个人的数据时,会加载出另外一个人的数据。
说明:主要还是看例子中对象所持有的属性对其的影响,不要把一对多和多对一的关系联系到“喜欢”上,重点在属性!!!哈哈
分割线==========================分割线==========================分割线
在这几种关联映射中,我觉得一对多这类关系映射最为复杂,所以在这里我先讲一对多这类(单向一对多,单向多对一,双向一对多)。有的人可能或疑惑,为什么没有双向多对一?其实在Hibernate中,一对多和多对一关系映射其实质是一样的,就是在“多“(一)的一端加对方的引用,指向“一”(多)的一端。不同的是,一对多是在“一”端加“多”端的集合,而多对一是在“多”端加“一”端的对象。所以双向的一对多和双向的多对一是一样的。
(因为下面的讲解会用到注解,如果对注解还不太熟悉的朋友可以先看一下注解及其属性讲解:Hibernate 关系映射注解详解)
下面的例子使用的是学生和班级的关系
1、单向多对一
学生和班级的关系就是多对一
1.1代码:
Student(多端)
@Entity
@Table(name = "ORM_Student")
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "ggs01")
@SequenceGenerator(name = "ggs01", initialValue = 1, allocationSize = 1)
private int id;// 学号
private String name;// 姓名
private String sex;// 性别
@ManyToOne(cascade = { CascadeType.ALL })
@JoinColumn(name = "grade_id")
private Grade grade_id;// 班级号(将学生和班级设计成多对一的关系,测试单向多对一的关系情况)
……(省略setter、getter方法)
……(省略构造方法、toString方法)
Grade(一端)
@Entity
@Table(name = "ORM_Grade")
public class Grade implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private int id;// 班级编号
private String name;// 班级名称
……(省略setter、getter方法)
……(省略构造方法、toString方法)
1.2测试代码:
(1)插入数据,生成表
/*
* 插入信息
*/
@Test
public void addGrade(){
//插入班级信息
Grade grade1 = new Grade(0701, "七年级1班");
Grade grade2 = new Grade(0702, "七年级2班");
Session session = sf.openSession();
Transaction tx = session.beginTransaction();
session.save(grade1);
session.save(grade2);
//插入学生信息
Student student1 = new Student("张三", "男",grade1);
Student student2 = new Student("李四", "男",grade1);
Student student3 = new Student("王五", "男",grade2);
session.save(student1);
session.save(student2);
session.save(student3);
tx.commit();
}
运行结果:
生成两张表 orm_studente 和 orm_grade ,其中 ORM_Student 表中含有一个外键 grade_id 对应 ORM_Grade 的主键。
(2)查询数据,比较加载内容
/*
* 查询班级信息
*/
@Test
public void queryGrade(){
Session session = sf.openSession();
Query query = session.createQuery("from Grade");
List list = query.list();
for(Grade g : list)
System.out.println(g);
}
/*
* 查学生信息
*/
@Test
public void queryStudent(){
Session session = sf.openSession();
Query query = session.createQuery("from Student");
List list = query.list();
for(Student g : list)
System.out.println(g);
System.out.println("=========下面测试加载多端时加载出来的一端=========");
Grade g = list.get(0).getGrade_id();
System.out.println(g);
}
运行结果:
1、 查询班级信息—>加载一的一端,没有其他信息被加载
Hibernate:
select
grade0_.id as id1_,
grade0_.name as name1_
from
ORM_Grade grade0_
Grade [id=449, name=七年级1班]
Grade [id=450, name=七年级2班]
2、 查学生信息—>加载多的一端,一的一端也被加载
Hibernate:
select
student0_.id as id0_,
student0_.grade_id as grade4_0_,
student0_.name as name0_,
student0_.sex as sex0_
from
ORM_Student student0_
Hibernate:
select
grade0_.id as id1_0_,
grade0_.name as name1_0_
from
ORM_Grade grade0_
where
grade0_.id=?
Hibernate:
select
grade0_.id as id1_0_,
grade0_.name as name1_0_
from
ORM_Grade grade0_
where
grade0_.id=?
Student [id=1, name=张三, sex=男, grade_id=Grade [id=449, name=七年级1班]]
Student [id=2, name=李四, sex=男, grade_id=Grade [id=449, name=七年级1班]]
Student [id=3, name=王五, sex=男, grade_id=Grade [id=450, name=七年级2班]]
=========下面测试加载多端时加载出来的一端=========
Grade [id=449, name=七年级1班]
比较结果:
从上面两个运行结果来看,在加载“一”的一端时,没有其他信息被加载;在加载“多”的一端,一的一端也被加载,这也就印证了上面开头的总结:加载负责维护关系的一端,系统会自动加载另一端。
2、单向一对多
单向一对多和单向多对一原理类似,就是只在“一”端添加“多”端对象的集合的引用(即在Grade对象中添加Set
测试代码和示例一的一样,只是运行的结果会不同:
① 发送的SQL语句不一样:单向一对多,如果将维护关系设置在“一”端,在运行时会比示例一多发送n条update语句(n的值为“多”端的记录条数)。
② 数据加载不同:单向一对多在加载多端时,不会加载出其他数据,而在加载一端时,会加载出多端的数据。
一般不使用单向一对多的原因:
1、如果在“多”端将“一”端的外键设置为非空,在插入数据的时候会报错。
报错原因:
将关系设置在“一”端时,向“多”端插入数据的时候,因为没有“一”端的数据,所以此时一端对应的外键是null,如果将数据库中“多”端的外键设置为非空,此时就会报错。
可以理解为,维护关系在“一”的一端,所以“多”的一端并不知道“一”的一端的存在,所以保存“多”的一端时,该数据表中“一”的一端的外键是空的。
2、当维护关系在“一”的一端时,操作“一”端时会多发送n条update语句。当操作的记录较多时,会增加系统消耗。
原因:
在上面已经有提及到,当保存“多”端数据时,开始并没有“一”端的值(即外键值为null),记录保存完毕后,系统会通过update更新的方式向“多”端中添加外键值,所以此时又会发送一条update语句。
3、双向一对多(不加mappedBy)
3.1代码:
Student(多端):
@Entity
@Table(name = "ORM_Student")
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "ggs01")
@SequenceGenerator(name = "ggs01", initialValue = 1, allocationSize = 1)
private int id;// 学号
private String name;// 姓名
private String sex;// 性别
@ManyToOne(cascade = { CascadeType.ALL })
@JoinColumn(name = "grade_id")
private Grade grade_id;// 班级号(将学生和班级设计成多对一的关系,测试单向多对一的关系情况)
……(省略setter、getter方法)
……(省略构造方法、toString方法)
Grade(一端):
@Entity
@Table(name = "ORM_Grade")
public class Grade implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private int id;// 班级编号
private String name;// 班级名称
@OneToMany(cascade={CascadeType.ALL})
@Column(name="student_id")
private Set students; //班级学生信息
……(省略setter、getter方法)
……(省略构造方法、toString方法)
3.2测试代码:
测试代码和示例1一样
3.3运行结果:
① 不加mappedBy的话,会生成三张表,有一张中间表。生成中间表的原因和示例2中的类似,Student对应的表中有一个外键,同理于示例2,因为主键的唯一性,在Grade中不会存在Student的外键,所以会生成一张中间表。-->关于mappedBy属性的介绍,见上文要点
② 在数据的加载方面,与单向最大的不同就是双向关系,加载任意一端,都会自动加载出另一端的数据。
4、双向一对多(加mappedBy)
该示例与示例3是一样的操作,不同的是需要在@OneToMany注解处加mappedBy属性。添加mappedBy属性后,不会生成第三张中间表。根据上述对生成第三张中间表的原因进行分析可以得到,主要是因为“一”的一端需要维护一对多的关系,也就是需要加载“多”的一端的数据,限于主键的唯一性,所以需要生成一张中间表(该表主要是为“一”端生成)。
在“一”端添加属性mappedBy后,就是意味着,将“一”端需要维护的关系转移到mappedBy所指定的对象(必须是“多”端配置的外键属性),相当于让“多”端帮忙打理这层关系,当加载“一”端时,按照双向关系的原理,是需要加载“多”端的,但是这时候并不是“一”端亲自去加载,而是通过“多”端协助加载。
-->关于mappedBy属性的介绍,见 Hibernate 关系映射注解详解