Hibernate 雙向多對多關(guān)聯(lián)映射
1. 前言
通過本節(jié)課程的學(xué)習(xí),你將發(fā)現(xiàn)關(guān)聯(lián)對象之間的微妙關(guān)系。相信這種關(guān)系對你更深入地認(rèn)識 HIbernate 有很大的幫助。
通過本節(jié)課程,你將了解到:
- 多對多雙向關(guān)聯(lián)映射中哪一方是關(guān)系維系者;
- 級聯(lián)操作與關(guān)系維系者。
2. 關(guān)系維系者
新學(xué)期開始了,同學(xué)們選擇了各自喜歡的課程,現(xiàn)在為學(xué)生添加選修課程的任務(wù)就要落在 Hibernate 的身上。一起來看看 Hibernate 是如何完成這個(gè)任務(wù)。
行動(dòng)之前,先假設(shè)一個(gè)需求:
“Hibernate” 同學(xué)覺得自己選修的課程太難了,現(xiàn)在想重新選擇。
重新選修之前,先刪除原來選修的內(nèi)容:
執(zhí)行流程分析:
- 進(jìn)入學(xué)生表,查詢到 “Hibernate” 同學(xué)的信息;
- 刪除 “Hibernate” 同學(xué)的所有選修課程。
2.1 解除級聯(lián)對象之間的關(guān)系
刪除方式有兩種:
第一種解除方案
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
// 查詢學(xué)生
Student student =(Student)session.get(Student.class, new Integer(1));
// 查詢Java課程
Course javaCourse = (Course) session.get(Course.class, new Integer(1));
// 查詢C課程
Course ccourse = (Course) session.get(Course.class, new Integer(2));
// 解除關(guān)系
student.getCourses().remove(javaCourse);
student.getCourses().remove(ccourse);
return null;
}
});
查詢到 ”Hibernate“ 同學(xué)的信息,注意,此時(shí) student 對象處于持久化狀態(tài),意味著 student 對象在程序世界的行為可以同步到數(shù)據(jù)庫中。
查詢 ”Hibernate“ 同學(xué)選修的 Java 和 C 兩門課程,此時(shí)保存這兩個(gè)課程信息的對象也處于持久化狀態(tài)。
使用如下代碼解除學(xué)生和課程之間的關(guān)系:
student.getCourses().remove(javaCourse);
student.getCourses().remove(ccourse);
因?yàn)?strong>學(xué)生對象、課程對象都處于持久化狀態(tài)。它們在程序世界中的一言一行都會(huì)同步到數(shù)據(jù)庫中。
既然在程序世界解除了彼此之間的關(guān)系,在數(shù)據(jù)庫中,中間表中的關(guān)系描述數(shù)據(jù)也會(huì)自動(dòng)刪除。
從控制臺上所顯示出來的 SQL 語句其實(shí)也知道刪除已經(jīng)成功,這個(gè)就不貼出來了。
進(jìn)入 MySql 驗(yàn)證一下:
中間表中已經(jīng)不存在和 ”Hibernate“ 同學(xué)相關(guān)的課程信息。
但是,此時(shí)你可能會(huì)有一個(gè)想法,剛剛是以學(xué)生對象為主動(dòng)方,向課程對象提出了分手,那么,能不能以課程方為主動(dòng)方提出分手呢?
試一下便知,測試之前,先恢復(fù)原來的內(nèi)容:
執(zhí)行下面的實(shí)例代碼:
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
// Hiberante學(xué)生
Student student =(Student)session.get(Student.class, new Integer(1));
// 查詢Java
Course javaCourse = (Course) session.get(Course.class, new Integer(1));
// 查詢C
Course ccourse = (Course) session.get(Course.class, new Integer(2));
// 解除關(guān)系,以課程對象為主動(dòng)方
javaCourse.getStudents().remove(student);
ccourse.getStudents().remove(student);
return null;
}
});
可能會(huì)讓你失望,這次操作對數(shù)據(jù)庫沒有任何影響。
可見,分手只能是由學(xué)生對象提出來。
雖然在前面課程中,咱們配置了學(xué)生類和課程類的雙向多對多關(guān)聯(lián)映射,但是,兩者之間只能有一個(gè)主動(dòng)方,這里要了解,所謂主動(dòng)方,就是關(guān)系的維系者。
關(guān)系的建立和解除只能由主動(dòng)方提供。是不是有點(diǎn)像霸道總裁的狗血故事。
第二種解除方案
Ok!一起繼續(xù)了解第 2 種方案。
不管是哪種方案,切記,只能是由主動(dòng)方提出分手。
行事之前,一定要先檢查數(shù)據(jù)是否存在。
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
// Hibernate學(xué)生
Student student =(Student)session.get(Student.class, new Integer(1));
//解除關(guān)系
student.getCourses().removeAll(student.getCourses());
return null;
}
});
和第一種方案相比較,不再查詢課程信息,由學(xué)生對象單方面一次性解除關(guān)系。
student.getCourses().removeAll(student.getCourses());
return null;
執(zhí)行結(jié)果沒有什么意外,程序世界中關(guān)系的解除操作同步到了數(shù)據(jù)庫中。
一起看看控制臺上輸出的信息:
Hibernate:
select
student0_.stuId as stuId1_1_0_,
student0_.stuName as stuName2_1_0_,
student0_.stuPassword as stuPassw3_1_0_,
student0_.stuPic as stuPic4_1_0_,
student0_.stuSex as stuSex5_1_0_
from
Student student0_
where
student0_.stuId=?
Hibernate:
select
courses0_.stuId as stuId1_1_1_,
courses0_.courseId as courseId2_2_1_,
course1_.courseId as courseId1_0_0_,
course1_.courseDesc as courseDe2_0_0_,
course1_.courseName as courseNa3_0_0_
from
score courses0_
inner join
Course course1_
on courses0_.courseId=course1_.courseId
where
courses0_.stuId=?
Hibernate:
delete
from
score
where
stuId=?
大家可以看到,Hibernate 接收到解除操作后,立即由中間表連接到課程表,把相關(guān)課程信息從中間表中抺出。
一切進(jìn)行得簡單而有序:
- 記住,持久化對象的行為可以同步到數(shù)據(jù)庫中去;
- 多對多雙向關(guān)聯(lián)映射中,有主動(dòng)方和被動(dòng)方一說。
2.2 建立級聯(lián)對象之間的關(guān)系
好!現(xiàn)在為 ”Hibernate“ 同學(xué)選修新的課程。比如說,想選擇 DB 課程和 JAVA 課程。
天呀,剛剛不是才解除了 JAVA 課程嗎。你管得著嗎,咱們就是這么任性,想解除就解除,想建立就建立 ,誰叫 Hibernate 這么方便呢。
有了前面的知識,應(yīng)該難不倒我們了。
現(xiàn)在來看看 Hibernate 實(shí)例:
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
// 添加新學(xué)生
Student student =(Student)session.get(Student.class, new Integer(1));
// 查詢Java
Course javaCourse = (Course) session.get(Course.class, new Integer(1));
// 查詢C
Course dbCourse = (Course) session.get(Course.class, new Integer(3));
// 確定關(guān)系
student.getCourses().add(javaCourse);
student.getCourses().add(dbCourse);
return null;
}
});
關(guān)系還是在程序中直接體現(xiàn),并且是由學(xué)生對象維護(hù)。
可以進(jìn)入數(shù)據(jù)庫,查看一下:
還是再叮囑一下,關(guān)系只能由學(xué)生方維護(hù)的。
此處,應(yīng)該有疑問,為什么只能是學(xué)生方,而不能是課程方,難道在 Java 的世界里也有命運(yùn)一說。
這個(gè)命運(yùn)的安排是由開發(fā)者來決定的,在進(jìn)行關(guān)聯(lián)注解時(shí),那一方使用了 mappedBy 屬性,則這一方就是被動(dòng)方,很好理解,這個(gè)屬性本身的含義就是說,聽對方的。
所以說,誰是霸道總裁,看開發(fā)者的心情。理論上講,多對多中,兩者應(yīng)該是平等關(guān)系。
剛剛的解除和重新建立都是對已經(jīng)存在的學(xué)生進(jìn)行的。
如果班里轉(zhuǎn)來了一名叫 ”HibernateTemplate“ 的新學(xué)生,他想選修 JAVA 和 DB,則應(yīng)該怎么操作呢?
很簡單??!
- 學(xué)生沒有,添加就是;
- 課程信息有,從表中查詢就是;
- 關(guān)系不存在,建立就是。
下面便是如你所想的實(shí)例代碼:
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
// 添加新學(xué)生
Student student = new Student("HibernateTemplate", "男");
// 查詢Java課程
Course javaCourse = (Course) session.get(Course.class, new Integer(1));
System.out.println(javaCourse.getCourseName());
// 查詢DB課程
Course dbCourse = (Course) session.get(Course.class, new Integer(3));
System.out.println(dbCourse.getCourseName());
// 確定關(guān)系
student.getCourses().add(javaCourse);
student.getCourses().add(dbCourse);
return null;
}
});
是的,這個(gè)代碼本身沒有問題。
但是,這里會(huì)有一個(gè)小坑,這個(gè)坑沒有填的話,你可能會(huì)測試不成功。
在學(xué)生類中,一定要記住對下面的屬性進(jìn)行實(shí)例化:
private Set<Course> courses=new HashSet<Course>();
否則就會(huì)有空指針異常拋出來。前面沒有,是因?yàn)閿?shù)據(jù)庫中有數(shù)據(jù),Hibernate 幫咱們自動(dòng)實(shí)例化了。
但現(xiàn)在是一個(gè)新學(xué)生,Hiberante 可不會(huì)幫你實(shí)例化他的課程集合屬性。
也就是不能任何時(shí)候都依靠 Hibernate,它也有顧全不到的地方。為了讓你寬心,還是看一下數(shù)據(jù)庫中數(shù)據(jù)吧:
3. 級聯(lián)刪除
前面講解雙向一對多的時(shí)候,也提到了級聯(lián)刪除。最大的印象就是,如果雙方都打開了級聯(lián)刪除,刪除時(shí)就如同推倒了多米諾骨牌的第一張牌,整個(gè)數(shù)據(jù)鏈都會(huì)刪除。
多對多關(guān)聯(lián)比一對多關(guān)聯(lián)多了一張中間表,在進(jìn)行級聯(lián)刪除的時(shí)候,到底會(huì)發(fā)生什么事情?
在此也有必要拿出來說一說。
為了不讓事情的發(fā)展如山崩一樣不可控制,先打開學(xué)生類的級聯(lián)操作功能:
private Set<Course> courses=new HashSet<Course>();
@ManyToMany(targetEntity = Course.class,cascade=CascadeType.ALL)
@JoinTable(name = "score", joinColumns = @JoinColumn(name = "stuId", referencedColumnName = "stuId"),
inverseJoinColumns = @JoinColumn(name = "courseId", referencedColumnName = "courseId"))
public Set<Course> getCourses() {
return courses;
}
這里使用 CascadeType.ALL。
來一段測試實(shí)例,刪除剛才添加的 HibernateTemplate 同學(xué)。
他會(huì)說我好悲慘,才進(jìn)來沒有多久。
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
// 查詢學(xué)生
Student student =(Student)session.get(Student.class, new Integer(23));
session.delete(student);
return null;
}
});
無驚無喜,一切按照預(yù)先的設(shè)想進(jìn)行。
刪除學(xué)生時(shí),中間表中與此學(xué)生有關(guān)聯(lián)的信息,也就是說此學(xué)生選修的課程信息也自動(dòng)被刪除了。
但是,會(huì)有一個(gè)想法,如果刪除課程,則中間表中記錄的與此課程有關(guān)的信息是否會(huì)自動(dòng)刪除呢?
OK!開始行動(dòng)之前,可別忘記在課程類中打開級聯(lián)操作選項(xiàng):
嘿嘿!現(xiàn)在兩邊的級聯(lián)操作功能都已經(jīng)打開。
private Set<Student> students=new HashSet<Student>();
@ManyToMany(targetEntity = Student.class, mappedBy = "courses",cascade=CascadeType.ALL)
public Set<Student> getStudents() {
return students;
}
打開后,執(zhí)行刪除 C 課程的實(shí)例,誰讓 C 不好學(xué)了。
HibernateTemplate<Course> hibernateTemplate = new HibernateTemplate<Course>();
hibernateTemplate.template(new Notify<Course>() {
@Override
public Course action(Session session) {
// 查詢學(xué)生
Course course =(Course)session.get(Course.class, new Integer(2));
session.delete(course);
return null;
}
});
這只是一個(gè)很簡單的代碼,但是卻發(fā)生如雪崩一樣的事件。
到底發(fā)生了什么事情?
大家進(jìn)入 MySql 看看就知道了。
3張表中空空如也,所有數(shù)據(jù)都沒有了。
就如同前面講解一對多的級聯(lián)刪除一樣。同樣適用于多對多關(guān)聯(lián)映射之中。
因兩邊都已經(jīng)打開了級聯(lián),刪除操作如同無法控制的壞情緒,刪除課程時(shí),以中間表為連接,反復(fù)來往于三張表,把相關(guān)信息全部刪除。
所以,使用級聯(lián)時(shí)一定要小心,否則,小心臟真的有點(diǎn)受不了。
4. 小結(jié)
好了,本節(jié)課可以結(jié)束了,通過本節(jié)課,大家了知道無論是雙向一對多,還是雙向多對多,都會(huì)有一個(gè)主動(dòng)方。
在雙向多對關(guān)聯(lián)中,主動(dòng)方有解除關(guān)系能力。
級聯(lián)操作功能用得好,能一勞永逸。但是,如果沒有用好,則會(huì)如同打開了潘多拉魔盒,發(fā)生雪崩一樣的災(zāi)難。