Hibernate 一對多雙向關(guān)聯(lián)映射
1. 前言
本節(jié)課程和大家一起聊聊一對多關(guān)聯(lián)映射。通過本節(jié)課程,你將了解到:
-
如何實現(xiàn)一對多關(guān)聯(lián)映射;
-
如何實現(xiàn)雙向一對多關(guān)聯(lián)映射;
-
關(guān)聯(lián)映射中的級聯(lián)操作。
2. 一對多關(guān)聯(lián)映射
關(guān)系型數(shù)據(jù)庫中表與表中的數(shù)據(jù)存在一對多(或多對一)關(guān)系。
如學(xué)生表、班級表。一個班級有多個學(xué)生,多個學(xué)生可以在同一個班級。
一對多或多對一本質(zhì)上是一樣的,如同一塊硬幣的正面和反面,只是看待事物的角度不同而已。
數(shù)據(jù)庫中有學(xué)生表、班級表。使用 Hibernate 進(jìn)行數(shù)據(jù)操作時, 程序中就應(yīng)該有學(xué)生類、班級類。
同時學(xué)生類、班級類應(yīng)該使用 OOP 語法描述出如同學(xué)生表和班級表一樣的關(guān)系。并且還要讓 Hibernate 看得懂。
有了前面的基礎(chǔ),直接上代碼:
創(chuàng)建班級類:
@Entity
public class ClassRoom {
private Integer classRoomId;
private String classRoomName;
private String classRoomDesc;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getClassRoomId() {
return classRoomId;
}
需求:查詢學(xué)生時,得到學(xué)生所在班級信息。
進(jìn)入學(xué)生類,添加如下代碼,描述學(xué)生類和班級類的關(guān)系:
private ClassRoom classRoom;
除此之外,還需要讓 Hibernate 知道,這個對象屬性的值來自于班級表中的對應(yīng)數(shù)據(jù),進(jìn)一步修改代碼:
private ClassRoom classRoom;
@ManyToOne(targetEntity=ClassRoom.class)
@JoinColumn(name="classRoomId")
public ClassRoom getClassRoom() {
return classRoom;
}
- @ManyToOne 告訴 Hibernate,從學(xué)生的角度來看,學(xué)生是多的一邊,查詢班級表可以得到學(xué)生所在班級信息。
- @JoinColumn 告訴 Hibernate 需要帶著指定的字段值到班級表中匹配數(shù)據(jù)。
修改 Hibernate 主配置文件中內(nèi)容:
<property name="hbm2ddl.auto">create</property>
<mapping class="com.mk.po.Student" />
<mapping class="com.mk.po.ClassRoom" />
為了讓事情變得簡單明了,在主配置文件中只保留學(xué)生類和班級類的映射關(guān)系。
學(xué)生類中的所有屬性描述:
private Integer stuId;
private String stuName;
private String stuSex;
private String stuPassword;
private Blob stuPic;
private ClassRoom classRoom;
@ManyToOne(targetEntity=ClassRoom.class)
@JoinColumn(name="classRoomId")
public ClassRoom getClassRoom() {
return classRoom;
}
使用上一節(jié)課的模板對象跑一個空測試實例:
@Test
public void testGetByTemplate() {
HibernateTemplate<Student> hibernateTemplate=new HibernateTemplate<Student>();
}
目的是讓 Hibernate 重新創(chuàng)建學(xué)生表、班級表。別忘記啦,自動創(chuàng)建表后,修改回:
<property name="hbm2ddl.auto">update</property>
進(jìn)入 MySql,在學(xué)生表、班級表中手工添加幾條測試數(shù)據(jù):
到了完成需求的時候,測試下面實例:
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
Student stu=(Student)session.get(Student.class, new Integer(1));
System.out.println("學(xué)生姓名:"+stu.getStuName());
System.out.println("學(xué)生所在班級:"+stu.getClassRoom().getClassRoomName());
return stu_;
}
});
控制臺輸出結(jié)果:
Hibernate:
select
student0_.stuId as stuId1_1_1_,
student0_.classRoomId as classRoo6_1_1_,
student0_.stuName as stuName2_1_1_,
student0_.stuPassword as stuPassw3_1_1_,
student0_.stuPic as stuPic4_1_1_,
student0_.stuSex as stuSex5_1_1_,
classroom1_.classRoomId as classRoo1_0_0_,
classroom1_.classRoomDesc as classRoo2_0_0_,
classroom1_.classRoomName as classRoo3_0_0_
from
Student student0_
left outer join
ClassRoom classroom1_
on student0_.classRoomId=classroom1_.classRoomId
where
student0_.stuId=?
學(xué)生姓名:Hibernate
學(xué)生所在班級:c1911
Hibernate 使用 left outer join 構(gòu)建了一條多表查詢語句!
3. 雙向一對多關(guān)聯(lián)映射
需求:查詢班級時,想知道班上有多少名學(xué)生,又應(yīng)該如何映射?
進(jìn)入班級類,添加如下屬性:
private Set<Student> students;
使用集合屬性 students,描述了一個班有多名學(xué)生。
為什么使用 Set 集合?
因為一個班級內(nèi)不可能出現(xiàn)兩個完全相同的學(xué)生對象。
這還僅僅只是 OOP 層面上的關(guān)系,還需要告訴 Hibernate 應(yīng)該如何填充數(shù)據(jù)。
添加下面代碼:
private Set<Student> students;
@OneToMany(targetEntity=Student.class,mappedBy="classRoom")
public Set<Student> getStudents() {
return students;
}
- @OneToMany:很直白的說明了一個班級會有多名學(xué)生,指引 Hibernate 在填充數(shù)據(jù)時,要找到所有學(xué)生,別遺漏了;
- 屬性 mappedBy=“classRoom”: 告訴 Hibernate,班級和學(xué)生之間的關(guān)系在學(xué)生類中已經(jīng)說的夠明白了,應(yīng)該不需要再廢話了吧。
OK!把前面的測試實例改為查詢班級信息:
HibernateTemplate<ClassRoom> hibernateTemplate = new HibernateTemplate<ClassRoom>();
hibernateTemplate.template(new Notify<ClassRoom>() {
@Override
public ClassRoom action(Session session) {
ClassRoom classRoom=(ClassRoom)session.get(ClassRoom.class, new Integer(1));
System.out.println("班級名稱:"+classRoom.getClassRoomName());
System.out.println("------我是分隔線------------------------");
System.out.println("班級學(xué)生人數(shù):"+classRoom.getStudents().size());
return classRoom;
}
});
查看控制臺輸出結(jié)果:
Hibernate:
select
classroom0_.classRoomId as classRoo1_0_0_,
classroom0_.classRoomDesc as classRoo2_0_0_,
classroom0_.classRoomName as classRoo3_0_0_
from
ClassRoom classroom0_
where
classroom0_.classRoomId=?
班級名稱:c1911
------我是分隔線------------------------
Hibernate:
select
students0_.classRoomId as classRoo6_0_1_,
students0_.stuId as stuId1_1_1_,
students0_.stuId as stuId1_1_0_,
students0_.classRoomId as classRoo6_1_0_,
students0_.stuName as stuName2_1_0_,
students0_.stuPassword as stuPassw3_1_0_,
students0_.stuPic as stuPic4_1_0_,
students0_.stuSex as stuSex5_1_0_
from
Student students0_
where
students0_.classRoomId=?
班級學(xué)生人數(shù):2
會發(fā)現(xiàn)一個很有意思的地方。Hibernate 查詢班級時,構(gòu)建了兩條 SQL。
先查詢班級,當(dāng)需要學(xué)生信息時,才構(gòu)建查詢學(xué)生的 SQL。
大家應(yīng)該也猜出來了,當(dāng)從學(xué)生表查詢班級表時,Hibernate 采用的是立即策略。
當(dāng)查詢從班級表查詢到學(xué)生表時,Hibernate 采用的是延遲加載策略。
采用延遲加載都只有一個目的,需要時加載,提高響應(yīng)速度。
現(xiàn)在,學(xué)生類和班級類的映射配置信息,能讓 Hibernate 自動從學(xué)生表查詢到班級表,也能從班級表查詢到學(xué)生表。
這種 2 個實體類中的映射關(guān)系就稱為雙向一對多關(guān)聯(lián)映射。
無論是 @ManyToOne 還是 @OneToMany 注解都有 fetch 屬性,可以設(shè)置的值有 2 個選擇:
- FetchType.EAGER
- FetchType.LAZY。
所以,在雙向一對多關(guān)聯(lián)映射可以選擇是否啟用延遲加載,這和一對一關(guān)聯(lián)映射中是一樣的,就不在此重復(fù)復(fù)述。
是否采用延遲加載,由項目邏輯決定。
4. 一對多關(guān)聯(lián)映射中的級聯(lián)操作
什么是級聯(lián)操作?
關(guān)系型數(shù)據(jù)庫中由主外鍵維系的兩張表,具有主從關(guān)系。
如學(xué)生表和班級表,班級班是主表,學(xué)生表是從表。
類似于刪除某一個班級的信息,則需要先刪除所在班的學(xué)生信息,再刪除班級信息,這個操作就是級聯(lián)操作。
所謂級聯(lián)操作,指操作一張表時,是否會牽連到與之有關(guān)聯(lián)的其它表。
現(xiàn)在,咱們是使用 Hibernate 進(jìn)行數(shù)據(jù)操作,不可能還要勞駕自己親力親為吧。只需要做些簡單配置,就可以讓 Hibernate 自動做級聯(lián)操作。
進(jìn)入班級類,修改代碼如下:
@OneToMany(targetEntity=Student.class,mappedBy="classRoom",cascade=CascadeType.REMOVE)
public Set<Student> getStudents() {
return students;
}
很簡單,只需要使用 @OneToMany 的 cascade 屬性,就能讓 Hibernate 明白如何做級聯(lián)操作。默認(rèn)情況下,沒有級聯(lián)效應(yīng)。
cascade 是一個枚舉類型:
public enum CascadeType {
ALL,
PERSIST,
MERGE,
REMOVE,
REFRESH,
DETACH
}
- ALL: 級聯(lián)所有操作;
- PERSIST: 級聯(lián)新增;
- MERGE: 級聯(lián)更新或者新增;
- REMOVE: 級聯(lián)刪除;
- REFRESH: 級聯(lián)刷新;
- DETACH: 級聯(lián)分離。
測試刪除班級實例:
HibernateTemplate<ClassRoom> hibernateTemplate = new HibernateTemplate<ClassRoom>();
hibernateTemplate.template(new Notify<ClassRoom>() {
@Override
public ClassRoom action(Session session) {
ClassRoom classRoom=(ClassRoom)session.get(ClassRoom.class, new Integer(1));
session.delete(classRoom);
return null;
}
});
如果不添加 cascade 相關(guān)說明,因為有學(xué)生引用班級信息,班級信息是不能被刪除的。
添加后再測試,查看表中內(nèi)容:班級以及班級所在學(xué)生信息全部刪除!
刪除班級時能級聯(lián)刪除學(xué)生,反過來,刪除學(xué)生能刪除班級嗎?
想法很好,實踐是檢驗真理的唯一手段,學(xué)生類中修改成如下代碼:
@ManyToOne(targetEntity=ClassRoom.class,cascade=CascadeType.REMOVE)
@JoinColumn(name="classRoomId")
public ClassRoom getClassRoom() {
return classRoom;
}
測試實例:
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
Student stu=(Student)session.get(Student.class, new Integer(2));
session.delete(stu);
return stu;
}
});
結(jié)果很殘酷!學(xué)生被刪除了,班級也被刪除了!
級聯(lián)級聯(lián),只要設(shè)置了級聯(lián),不管刪除學(xué)生還是班級,只要在對應(yīng)表中有引用關(guān)系的數(shù)據(jù)就會被刪除。
現(xiàn)在,學(xué)生類、班級類中的級聯(lián)刪除都打開了。如果對下面情形的數(shù)據(jù)(編號 1、2 的學(xué)生的班級編號都為 1)進(jìn)行刪除操作,則會發(fā)生什么事情?
數(shù)據(jù)庫中的數(shù)據(jù)如下:
測試刪除編號為 1 的學(xué)生:
HibernateTemplate<Student> hibernateTemplate = new HibernateTemplate<Student>();
hibernateTemplate.template(new Notify<Student>() {
@Override
public Student action(Session session) {
Student stu=(Student)session.get(Student.class, new Integer(1));
session.delete(stu);
return stu;
}
});
進(jìn)入 MySql,查看一下:
天呀!這是級聯(lián)還是株連呀,太讓人后怕,數(shù)據(jù)都沒有了。
刪除學(xué)生時,會級聯(lián)刪除和學(xué)生有關(guān)的班級,班級刪除時,又會查看學(xué)生表中是否還存在與班級有關(guān)聯(lián)的學(xué)生,有,則一刀下去,連根拔起。
Hibernate 有點剎不住車,產(chǎn)生了級聯(lián)連鎖反應(yīng)。
針對上面的測試,如果班級表的級聯(lián)關(guān)閉,執(zhí)行測試代碼,請問結(jié)果又會怎樣?
本節(jié)課程,講解了級聯(lián)刪除,級聯(lián)添加的內(nèi)容留到下節(jié)課繼續(xù)展開。
5. 小結(jié)
本節(jié)課和大家聊了雙向一對多關(guān)聯(lián)映射。
無論是一對一雙向關(guān)聯(lián)映射,還是一對多雙向關(guān)聯(lián)映射。都可以根據(jù)需要隨時設(shè)置是否延遲加載、級聯(lián)等操作。
在使用級聯(lián)操作時,一定要小心,避免產(chǎn)生連鎖反應(yīng),刪除了不應(yīng)該刪除的數(shù)據(jù)。