• 关联关系,是使用最多的一种关系,非常重要。在内存中反映为实体关系,映射到DB中为主外键关系。实体间的关联,即对外键的维护。关联关系的发生,即对外键数据的改变。
  • 外键:外面的主键,即,使用其他表的主键值作为自己的某字段的取值。
  • 在一对多关联关系中,外键总是被定义在多方表中。例如,国家Country与城市City间的关系就属于一对多关联关系,外键字段一般情况下是被定义在City表中的。

1 基本概念

1.1 关联属性

  • java代码的实体类定义中,声明的另一个实体类类型或者其集合类型的属性,称之为关联属性。

1.2 级联操作

  • 当对某一类的对象a进行操作,如增加、删除、修改时,同时会对另一类的某对象b进行相同的操作。此时称,对象a、b具有级联关系,对象b为对象a的级联对象。
  • 级联操作是通过映射文件中的cascade属性设置的。该属性的值较多,其介绍如下: 1、save:在保存、更新或者删除当前对象时,忽略其他关联的对象,即不使用级联。它是默认值。 2、save-update:当通过Session的save()、update()、saveOrUpdate()方法来保存或者更新当前对象时,将级联到其他DB中的相关联的表。 3、delete:当通过Session的delete()方法删除当前对象时,将级联删除所有关联的对象。 4、all:包含save-update以及delete级联的所有行为。另外,当对当前对象执行lock()操作时,也会对所有关联的持久化对象执行lock()操作。 5、delete-orphan:删除所有和当前对象解除关联关系的对象。 6、all-delete-orphan:包含all和delete-orphan级联的所有行为。

1.3 关联关系维护

  • 关联关系的维护,也称之为外键维护,即为外键字段赋值。Hibernate默认情况下,关联的双方都具有维护权。即在代码中均可通过自己关联属性的set方法来建立关联关系。反映到数据库中,即是为外键字段赋值。
  • 在1:n关系中,例如Country和部长Minister的关系中:
  • Country对象可以调用自己的setMinister()方法来建立关联关系,Minister也可以调用自己的setCountry()方法来建立关联关系。
  • 不过,由于外键是建立在多方表minister中的,所以对于外键的维护方式,即为外键字段赋值的方式,一方维护与多方维护,其底层执行是不同的。
  • 若关联关系由一方维护,即Country对象执行country.setMinisters(ministers)方法,其实质是country对象为minister表的外键countryId赋值,底层是通过update语句来完成的。
  • 底层为什么是通过update来完成维护的呢?country要主动关联ministers,则需要在country对象产生之前,先在DB的minister表中将即被关联的minister先插入完成。此时DB的表中的minister的countryId字段值一定为null。当country对象产生后,需要执行update语句来修改这个minister表的countryId的值。
  • 若关联关系由多方维护,即Minister对象执行minister.setCountry(country)方法,其实质是minister对象为自己的表的外键赋值,则可在插入minister数据时一并完成,即通过insert语句来完成。
  • 为什么这里又是通过inster语句完成关联关系维护的呢?minister要主动关联country,那么在minister出现之前就需要先在DB的表中插入完毕将要被关联country,后再插入minister。所以,在Insert这个主关联对象minister的同时,将countryId的值也放入了DB中。
  • 虽然双方均具有维护权,但是一方同时具有放弃维护权的特权。通过对一方关联属性inverse="true"设置,即可放弃关联关系维护权,将维护权完全交给多方。

1.4 预处理语句

  • 所谓预处理的语句,即该语句当前先产生,但是暂时不执行,等后面条件成熟,或者程序运行完毕再执行的语句。
  • 当一方具有关联关系的维护权,并且执行save(一方对象)时,会产生一条update预处理语句,用于维护外键值。那么,为什么这个update为预处理语句,而不是立即执行呢?因为该语句所要update的这条多方表中记录还未被插入,即还不存在。只有当这个多方对象也insert完毕后,即在多方表中出现这条语句时,才会引发预处理update的执行,将多方表中的外键字段值填上。
  • 当多方具有关联关系的维护权,并且执行save(多方对象)时,会产生一条insert预处理语句,用于维护外键值。那么,为什么这个insert也为预处理语句,而不是立即执行的呢?因为该语句所要insert的这条多方数据,其所关联的一方对象还未被插入,即还不存在。所以其外键字段值还未出现。只有当这个一方对象也insert完毕后,即在一方表中出现这条记录时,才会引发对多方对象的预处理语句insert的执行,将多方表中的外键字段值同多方表中的其他普通属性值一同插入。

1.5 关联方向

1.5.1 单向关联

  • 指具有关联关系的实体对象间的加载和访问关系是单向的。即只有一个实体对象可以加载和访问对象,但是对方是看不到另一方的。

1.5.2 双向关联

  • 指具有关联关系的实体对象间的加载和访问关系是双向的。即任何一方均可加载和访问另一方。

1.6 关联数量

  • 实体对象间的关系,从数量上可以划分为:1:1、1:n、n:1、m:n。

2 关系映射

  • 以下双向关联举例代码中,在定义实体类的toString()方法时需要注意,对关联属性的输出,最好是只有一方进行输出,而另一方不进行关联属性输出。因为双方均进行输出,有可能出现循环引用问题,会抛出栈溢出错误StackOverflowError。

2.1 1:n-单向关联

  • 举例:one2many_s 国家(Country)和部长(Minister) 1、实体类中的定义
package com.eason.hibernate.po;
import java.util.HashSet;
import java.util.Set;
public class Country {
	private Integer cid;
	private String cname;
	private Set<Minister> ministers;
	
	//setter and getter()
	
	public Country(String cname) {
		this();
		this.cname = cname;
	}
	public Country() {
		super();
		ministers = new HashSet<>();
	}
	@Override
	public String toString() {
		return "Country [cid=" + cid + ", cname=" + cname + ", ministers=" + ministers + "]";
	}
}
package com.eason.hibernate.po;
public class Minister {
	private Integer mid;
	private String mname;

  //setter and getter()

	public Minister(String mname) {
		super();
		this.mname = mname;
	}
	public Minister() {
		super();
		// TODO Auto-generated constructor stub
	}
	@Override
	public String toString() {
		return "Minister [mid=" + mid + ", mname=" + mname + "]";
	}
}

2、映射文件中的配置

  • Country类的关联属性再映射文件中配置如下:
	<class name="Country" table="t_country">
		<id name="cid">
			<generator class="native"></generator>
		</id>
		<property name="cname"></property>
		<set name="ministers" cascade="save-update">
			<key column="country_id"></key>
			<one-to-many class="Minister"/>
		</set>
	</class>
  • set:指明name指定关联属性ministers的映射为集合映射;
  • <one-to-many>与<key>:在Minister类的映射表中产生名为country_Id的外键。注意,Minister表中并无此映射,是由这里指定生成的。one-to-many标签,指明当前类Country与class指定类Minister的关系为1:n。
  • Minister类的关联属性再映射文件中的配置如下:
	<class name="Minister" table="t_minister">
		<id name="mid">
			<generator class="native"></generator>
		</id>
		<property name="mname"></property>
	</class>

2.1.1 代码运行分析一

1、运行条件:Country映射文件<set>中不设置cascade="save-update",测试类中在save(country)之前,也不做save(minister)。

			Minister minister = new Minister("aaa");
			Set<Minister> ministers = new HashSet<Minister>();
			ministers.add(minister);
			Country country = new Country("USA");
			country.setMinisters(ministers);
			session.save(country);

2、运行结果:运行报错,对象引用了一个未保存的瞬时态实例。 3、过程分析:

  • 在save(country)时,发现inverse为false,即会产生一条update预处理语句。当执行对country的insert后,程序执行完毕,此时会执行预处理语句。而预处理语句是要对t_minister表操作,而此前无任何对Minister对象的insert语句,即DB中是不存在该对象的。该对象现只存在于普通内存中,与session无关,DB中没有,即处于瞬时态。所以报错:引用了没有保存的瞬时态实例。 2.1.2 代码运行分析二 1、运行条件:测试类中在save(country)之前,先执行save(minister);或者在country的映射文件中的关联属性映射中增加级联操作cascade="save-update"。
  • 测试类中在save(country)之前,先执行save(minister)。
			Minister minister = new Minister("aaa");
			Set<Minister> ministers = new HashSet<Minister>();
			ministers.add(minister);
			Country country = new Country("USA");
			country.setMinisters(ministers);
			session.save(minister);
			session.save(country);
  • 或者country的映射文件中增加cascade="save-update"。

2、运行结果:两种情况的运行过程和结果完全相同,运行均成功。


3、过程分析:

  • 先执行minister的insert,此时还没有country对象,所以,也就不会为外键赋值,仅仅为mname赋值。
  • 再执行country的insert,发现inverse为默认值false,所以产生预处理的update,并完成insert。
  • 当country的insert完成后,对于minister外键的维护条件完成,所以执行预处理的update。

2.1.3 代码运行分析三

1、运行条件:

  • 在country的映射文件中的关联属性映射中增加控制反转设置inverse="true";
  • 在country的映射文件中的关联属性映射中增加设置级联操作,或者在测试类中进行save(minister)。

2、运行成功,但是t_minister表中外键值为null。 3、过程分析:

  • 当执行country的insert,发现inverse为true,故将外键维护权交给多方,即minister,完成country的insert。
  • 当country的insert完成后,进行级联保存minister,即要执行对minister的insert,不过,此时minister具有对外键的维护权,需要为外键赋值。但是由于是单向关联,Minister看不到Country,即没有setCountry()方法,所以只能插入mname的值,外键没有赋值,即为null。

2.2 1:n-双向关联

  • 举例:one2many_d 国家(Country)与部长(Minister)
  • 本例中在进行两个实体定义时需要注意,若Country的toString()方法中对其关联属性mimisters进行了输出,那么Minister的toString()方法就不要再输出其关联属性country了。 1、定义实体类:
	package com.eason.hibernate.po;
	import java.util.HashSet;
	import java.util.Set;

	public class Country {
		private Integer cid;
		private String cname;
		private Set<Minister> ministers;

		//setter and getter()

		public Country(String cname) {
			this();
			this.cname = cname;
		}
		public Country() {
			super();
			ministers = new HashSet<>();
		}
		@Override
		public String toString() {
			return "Country [cid=" + cid + ", cname=" + cname + ", ministers=" + ministers + "]";
		}

	}
	package com.eason.hibernate.po;

	public class Minister {
		private Integer mid;
		private String mname;
		private Country country;

		//setter and getter()

		public Minister(String mname) {
			super();
			this.mname = mname;
		}
		public Minister() {
			super();
			// TODO Auto-generated constructor stub
		}
		@Override
		public String toString() {
			return "Minister [mid=" + mid + ", mname=" + mname + "]";
		}
	}

2、映射文件中的设置:

  • 在Minister类的映射文件中关联关系映射如下:
	<class name="Minister" table="t_minister">
		<id name="mid">
			<generator class="native"></generator>
		</id>
		<property name="mname"></property>
		<many-to-one name="country" class="Country" column="country_id"></many-to-one>
	</class>
  • 其中,name指的是关联属性;column指的是关联属性对应的关联字段,即minister表的外键字段,该字段名为country映射文件<set/>中的<key/>字段同名;class指的是关联属性所对应的类型。
  • 在Country类的映射文件中关联关系映射如下:
	<class name="Country" table="t_country">
		<id name="cid">
			<generator class="native"></generator>
		</id>
		<property name="cname"></property>
		<set name="ministers" cascade="save-update" inverse="true">
			<key column="country_id"></key>
			<one-to-many class="Minister"/>
		</set>
	</class>
  • 一对多双向关联在设置多方的级联时需要注意,一般不设置删除级联。避免删除多方中的一个元素,而将所有内容全删。

2.2.1 代码运行分析一

1、运行条件:

  • 在country的映射文件中的关联属性映射中增加控制反转设置inverse="true"与级联操作cascade="save-update"。
  • 在minister的映射文件中增加<many-to-one/>标签。
  • 测试类中增加minister.setCountry(country)。
  • 测试类中值save(country),而不进行save(minister)。
				Minister minister = new Minister("aaa");
				Set<Minister> ministers = new HashSet<Minister>();
				ministers.add(minister);
				Country country = new Country("USA");
				country.setMinisters(ministers);
				minister.setCountry(country);
				session.save(country);

2、运行结果:运行成功,表中数据正确。 3、过程分析:

  • 当执行country的insert,发现inverse为true,故将外键维护权交给多方,即minister。完成country的insert。
  • 当country的insert完成后,进行级联保存minister,即要执行对minister的insert。代码中minister执行setCountry()方法,且minister具有外键维护权,所以在插入时直接将外键值写入DB中。

2.2.2 代码运行分析二

1、运行条件:

  • 在country的映射文件中的关联属性映射中增加控制反转设置“inverse="true"与级联操作。
  • 在minister的映射文件中增加<many-to-one/>标签中增加级联操作。
  • 测试类中只save(minister),而不进行save(country)。
	<class name="Country" table="t_country">
		<id name="cid">
			<generator class="native"></generator>
		</id>
		<property name="cname"></property>
		<set name="ministers" cascade="save-update" inverse="true">
			<key column="country_id"></key>
			<one-to-many class="Minister"/>
		</set>
	</class>
	<class name="Minister" table="t_minister">
		<id name="mid">
			<generator class="native"></generator>
		</id>
		<property name="mname"></property>
		<many-to-one name="country" class="Country" column="country_id" cascade="save-update"></many-to-one>
	</class>
	Minister minister = new Minister("aaa");
				Set<Minister> ministers = new HashSet<Minister>();
				ministers.add(minister);
				Country country = new Country("USA");
				country.setMinisters(ministers);
				minister.setCountry(country);
				session.save(minister);

2、运行结果:运行成功,表中的数据正确。 3、过程分析:

  • 当执行minister的insert,发现外键维护权交由自己,即多方控制,故需要将外键值和普通数据一起插入DB。而此时尚无外键关联的对象Country,所以先将minister的insert语句变成预处理语句存起来等条件成熟再执行。故真正执行的是其级联的对Country的insert。
  • 对country的insert执行完毕,预处理insert执行条件完成,执行该预处理insert语句。

2.3 自关联

  • 所谓自关联是指,机子即充当一方,又充当多方,是1:n的变型。例如,对于新闻栏目Column,可以充当一方,即父栏目,也可以充当多方,即子栏目。而反映到DB表中,只有一张表,这张表中具有一个外键,用于表示该栏目的父栏目。一级栏目的外键值为NULL,而子栏目则具有外键值。
  • 举例:one2many_oneself

2.3.1 定义实体类

	package com.eason.hibernate.po;
	import java.util.HashSet;
	import java.util.Set;
	public class NewColumn {
		private Integer id;
		private String name;   //栏目名称
		private String content;   //栏目内容
		private NewColumn parentNewColumn;  //父栏目
		private Set<NewColumn> childrenNewColumn;  //子栏目

		public NewColumn() {
			childrenNewColumn = new HashSet<NewColumn>();
		}

		public NewColumn(String name) {
			this();
			this.name = name;
		}

		//setter and getter()

		@Override
		public String toString() {
			return "NewColumn [id=" + id + ", name=" + name + ", content=" + content + ", parentNewColumn="
					+ parentNewColumn + "]";
		}
	}

2.3.2 定义映射文件

	<class name="NewColumn" table="t_column">
		<id name="id">
			<generator class="native"></generator>
		</id>
		<property name="name"></property>
		<property name="content"></property>
		<!-- 多方关联属性,多对一映射 -->
		<many-to-one name="parentNewColumn" class="NewColumn" column="pid" cascade="save-update"></many-to-one>
		<!-- 一方关联属性 -->
		<set name="childrenNewColumn" cascade="save-update">
			<key column="pid"></key>
			<one-to-many class="NewColumn"/>
		</set>
	</class>

2.3.3 定义测试类

		NewColumn footballNewColumn = new NewColumn("足球栏目");
		footballNewColumn.setContent("足球栏目足球栏目足球栏目");

		NewColumn basketballNewColumn = new NewColumn("篮球栏目");
		basketballNewColumn.setContent("篮球栏目篮球栏目篮球栏目");

		NewColumn sportsNewColumn = new NewColumn("体育栏目");
		sportsNewColumn.setContent("体育栏目体育栏目体育栏目");
		sportsNewColumn.getChildrenNewColumn().add(footballNewColumn);
		sportsNewColumn.getChildrenNewColumn().add(basketballNewColumn);

		session.save(sportsNewColumn);

2.4 n:1单向关联

  • 举例:many2one_s2 部长(Minister)和国家(Country)

2.4.1 定义实体类

	package com.eason.hibernate.po;

	public class Minister {
		private Integer mid;
		private String mname;
		private Country country;

		//setter and getter()

		public Minister(String mname) {
			super();
			this.mname = mname;
		}
		public Minister() {
			super();
			// TODO Auto-generated constructor stub
		}
		@Override
		public String toString() {
			return "Minister [mid=" + mid + ", mname=" + mname + ", country=" + country + "]";
		}
	}
	package com.eason.hibernate.po;
	import java.util.HashSet;
	import java.util.Set;
	public class Country {
		private Integer cid;
		private String cname;

		//setter and getter()

		public Country(String cname) {
			this.cname = cname;
		}
		public Country() {
			super();
		}
	}

2.4.2 映射文件中的设置

  • 在Minister类的映射文件中关联关系映射如下:
	<class name="Minister" table="t_minister">
		<id name="mid">
			<generator class="native"></generator>
		</id>
		<property name="mname"></property>
		<many-to-one name="country" class="Country" column="country_id" cascade="save-update"></many-to-one>
	</class>
  • 在Country类的映射文件中关联关系映射如下:
	<class name="Country" table="t_country">
		<id name="cid">
			<generator class="native"></generator>
		</id>
		<property name="cname"></property>
	</class>

2.4.3 定义测试类

		Minister minister = new Minister("aaa");
		Country country = new Country("USA");
		minister.setCountry(country);
		session.save(minister);

2.5 n:m-单向关联

  • 举例:many2many_s 学生(Student)与课程(Course)
  • 多对多的关联关系是通过增加一个中间表的方式来实现的。如,本例增加了选课表t_middle作为中间表。

2.5.1 定义实体类

	package com.eason.hibernate.po;
	import java.util.HashSet;
	import java.util.Set;
	public class Student {
		private Integer sid;
		private String sname;
		private Set<Course> courses;

		public Student() {
			courses = new HashSet<>();
		}
		public Student(String sname) {
			this();
			this.sname = sname;
		}

		//setter and getter()

		public Student(Integer sid, String sname, Set<Course> courses) {
			super();
			this.sid = sid;
			this.sname = sname;
			this.courses = courses;
		}
	}
	package com.eason.hibernate.po;
	public class Course {
		private Integer cid;
		private String cname;

		public Course() {
		}
		public Course(String cname) {
			super();
			this.cname = cname;
		}
		@Override
		public String toString() {
			return "Course [cid=" + cid + ", cname=" + cname + "]";
		}

		//setter and getter()
	}

2.5.2 映射文件中的设置

  • 在Student类的映射文件中关联关系映射如下:
	<class name="Student" table="t_student">
		<id name="sid">
			<generator class="native"></generator>
		</id>
		<property name="sname"></property>
		<set name="courses" table="t_middle" cascade="save-update">
			<key column="student_id"></key>
			<many-to-many class="Course" column="course_id"></many-to-many>
		</set>
	</class>
  • 在Course类的映射文件中关联关系映射如下:
	<class name="Course" table="t_course">
		<id name="cid">
			<generator class="native"></generator>
		</id>
		<property name="cname"></property>
	</class>

2.5.3 定义测试类

			Course course1 = new Course("Struts2");
			Course course2 = new Course("Hibernate");

			Student student = new Student("aaa");
			student.getCourses().add(course1);
			student.getCourses().add(course2);
			session.save(student);

2.6 n:m-双向关联

  • 举例:many2many_d 学生与课程(Course)。
  • 多对多的双向关联,使得双方地位完全相同。由于双方配置相同,所以在测试类中只要设置好了关联关系,对哪一方进行save()操作均可完成对双方的保存。

2.6.1 定义实体类

	package com.eason.hibernate.po;
	import java.util.HashSet;
	import java.util.Set;
	public class Student {
		private Integer sid;
		private String sname;
		private Set<Course> courses;

		public Student() {
			courses = new HashSet<>();
		}
		public Student(String sname) {
			this();
			this.sname = sname;
		}

	 //setter and getter()

		public Student(Integer sid, String sname, Set<Course> courses) {
			super();
			this.sid = sid;
			this.sname = sname;
			this.courses = courses;
		}
	}
	package com.eason.hibernate.po;

	import java.util.HashSet;
	import java.util.Set;

	public class Course {
		private Integer cid;
		private String cname;
		private Set<Student> students;

		public Course() {
			students = new HashSet<>();
		}
		public Course(String cname) {
			this();
			this.cname = cname;
		}
		@Override
		public String toString() {
			return "Course [cid=" + cid + ", cname=" + cname + "]";
		}

		//setter and getter()
	}

2.6.2 定义配置文件

  • 在Student类的映射文件中关联关系映射如下:
	<class name="Student" table="t_student">
		<id name="sid">
			<generator class="native"></generator>
		</id>
		<property name="sname"></property>
		<set name="courses" table="t_middle" cascade="save-update">
			<key column="student_id"></key>
			<many-to-many class="Course" column="course_id"></many-to-many>
		</set>
	</class>
  • 在Course类的映射文件中关联关系映射如下:
	<class name="Course" table="t_course">
		<id name="cid">
			<generator class="native"></generator>
		</id>
		<property name="cname"></property>
		<set name="students" table="t_middle" cascade="save-update">
			<key column="course_id"></key>
			<many-to-many class="Student" column="student_id"></many-to-many>
		</set>
	</class>

2.6.3 定义测试类

			Course course = new Course("Spring");	
			Student student = new Student("aaa");
			course.getStudents().add(student);
			session.save(course);