Hibernate对象关系映射详解之一对多关系映射

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集合变量以及该变量的settergetter方法),并且在getter上添加注解:@OneToMany(cascade={CascadeType.ALL})


测试代码和示例一的一样,只是运行的结果会不同:

发送的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 关系映射注解详解







你可能感兴趣的:(hibernate)