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