- 关联关系,是使用最多的一种关系,非常重要。在内存中反映为实体关系,映射到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);