- 关联关系,是使用最多的一种关系,非常重要。在内存中反映为实体关系,映射到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);
转载于:https://blog.51cto.com/12402717/2086393