Hibernate Lazy&Fetch
1. 前言
本節和大家一起聊聊 Hibernate 中的 Lazy 和 Fetch 的區別,及兩者適合的開發場景。通過本節課程的學習,你將了解到:
- 什么是延遲加載;
- 延遲加載的意義。
2. 又見 get() 和 load()
Session 對象提供了 2 個方法用來查詢 :
- get() 方法;
- load()方法。
如果僅以結果為導向,則無法分辨兩者的差異性。
兩者如同雙胞胎,外觀雖然差異不大,但其神韻各有千秋。仔細辨別,便能發現屬于各自的特征。
真相只有一個,查明真相的手段,也只有一種:讓代碼回答。
2.1 測試 get() 方法
Student stu=null;
try{
// 打開事務
transaction = session.beginTransaction();
//使用get()方法查詢學號為1的學生
stu=(Student)session.get(Student.class, new Integer(1));
System.out.println("--------------輸出學生信息------------------");
System.out.println(stu.getStuName());
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
System.out.println("***********關閉Session之后******************");
System.out.println(stu.getStuName());
如上測試代碼,和上一節課程的 get() 方法測試有區別:
- 調用 **get()** 方法查詢編號為 1 的學生數據,但會在輸出學生數據之前先輸出一條提示語句,作為標識分割線;
- 關閉 Session 對象后繼續使用查詢出來的學生數據。
查看代碼運行結果:
select
student0_.stuId as stuId1_0_0_,
student0_.stuName as stuName2_0_0_,
student0_.stuPassword as stuPassw3_0_0_,
student0_.stuPic as stuPic4_0_0_,
student0_.stuSex as stuSex5_0_0_
from
Student student0_
where
student0_.stuId=?
--------------輸出學生信息------------------
Hibernate是老大
***********關閉Session之后******************
Hibernate是老大
結果能說明什么問題呢?
仔細分析輸出的日志信息:
-
調用 get() 方法時,Hibernate 就構建了一條 Sql 語句。說明,調用 get() 方法時,Hibernate 就跑了一趟數據庫,并拿到了開發者指定的數據;
-
關閉 Session 對象后,程序可以繼續使用學生數據。說明,通過 get() 方法獲得的數據已經保存到程序運行的內存中,不需要再依賴 Session。
想說明什么?不想說明什么?只是一個結論。
2.2 測試 load() 方法
把上面測試實例中的 get() 方法換成 load() 方法。
且運行實例:
Student stu=null;
try{
// 打開事務
transaction = session.beginTransaction();
//查詢學號為1的學生
stu=(Student)session.load(Student.class, new Integer(1));
System.out.println("--------------輸出學生信息------------------");
System.out.println(stu.getStuName());
transaction.commit();
} catch (Exception e) {
transaction.rollback()
} finally {
session.close();
}
System.out.println("***********關閉Session之后******************");
System.out.println(stu.getStuName());
控制臺上查看實例使用結果:
--------------輸出學生信息------------------
Hibernate:
select
student0_.stuId as stuId1_0_0_,
student0_.stuName as stuName2_0_0_,
student0_.stuPassword as stuPassw3_0_0_,
student0_.stuPic as stuPic4_0_0_,
student0_.stuSex as stuSex5_0_0_
from
Student student0_
where
student0_.stuId=?
Hibernate是老大
***********關閉Session之后******************
Hibernate是老大
得到什么結論了嗎?
現在開始尋找區別。
不仔細觀察,會誤判沒有什么區別。
而其中有一個很明顯的區別就是:
調用 load ( ) 方法時,Hiberante 并沒有真正的行動,只有當執行到下面代碼時:
System.out.println(stu.getStuName());
Hibernate 才從容不迫地構建 Sql 語句,往數據庫跑了一趟,獲得數據,再輸出數據。
OK!再稍微改動一下測試代碼:
//會話對象
Student stu=null;
try {
// 打開事務
transaction = session.beginTransaction();
//查詢學號為1的學生
stu=(Student)session.load(Student.class, new Integer(1));
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
session.close();
}
System.out.println("***********關閉Session之后******************");
System.out.println("--------------輸出學生信息------------------");
System.out.println(stu.getStuName());
輸出學生信息并不是在調用 load( ) 方法之后,而是關閉 Session 對象之后,結果又會怎樣?猜得出來嗎?
把你心中的猜想和下面的輸出結果比較一下。
沒想到吧,拋異常啦,拋異常沒什么大驚小怪的,異常是為了告訴你錯誤原因。查看異常信息,從中找出原因:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:164)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:285)
at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:185)
省略其它若干……
不要望而生畏,都是紙老虎。找出關鍵詞:
could not initialize proxy - no Session
初看字面意思:不能初始化代理,沒有 Session 對象。
什么意思?
到了好好解釋這個原因的時候:
調用 load( ) 方法時,Hibernate 根本沒有如你所期望一樣,往數據庫跑。但是又不想讓你知道它沒去,或是怕你擔心它是否能完成這份工作。于是 Hibernate 為你提供了一個 代理對象。
這里會涉及到代理設計模式!為不影響主題學習,代理設計模式相關內容自己了解一下。
什么是代理對象?
通俗講就是說外觀和開發者所期望的對象一樣,但沒有實質性數據。
再通俗點,就是一個替身。有其外形,而無內涵。
再通俗講:哦,你已經明白了。
Hibernate 這是演的哪一出?這不是擺明欺負人嗎?
別誤會,這是 Hibernate 的善意之舉!善意從何談起呀!別急!
只有當開發者真正需要數據時:
System.out.println(stu.getStuName());
Hibernate 才會構建 Sql 語句,往數據庫跑一趟,獲得真正的數據。
但是,執行 Sql 語句是一定要在 Session 的生命周期之內,如果:
session.close();
通往數據庫的橋梁被拆了。Hibernate 也無能為力,只能以異常的方式告訴你:
no Session!臣妾做不到呀。
測試 get()、load() 方法的輸出結果已經表明了兩者的差異性:
- get() 方法言行一致,說出手呀便出手。開發者一調用,便快馬加鞭,從數據庫中獲得數據,保存到學生對象中,只要學生對象在,數據也就在;
- load() 方法,看起來倒像是說一套,做一套的主。并不會立馬行事,而是創建一個學生代理對象,提供和開發者期待的學生數據對象相同的方法接口,不影響開發者調用。只有當開發者真正需要數據時,才會說,好的,我去看一下數據庫。
故而,使用 load() 時就需要特別注意,在 Hibernate 取數據庫之前,千萬別關閉通向數據庫的橋梁:Session 對象。
Session 家里有 2 個可用于查詢的兄弟:
- get() 是老實人,言行一致。
- load() 有點小調皮,有時搞點惡作劇,但心思并不壞。如果真正理解它的意圖,在特定的環境下,可能會感動到你。
其實兩兄弟都很有趣。
3. 延遲加載
延遲加載?不是在聊 get() 和 load() 方法嗎,不是聊得好好的嘛!咋的,中場休息呀。
3.1 什么是延遲加載
什么是延遲加載?前面的測試結論已經給出了答案。
使用 Hibernate 獲取數據時,有時,Hibernate 并不急著去數據庫,而是等到開發者真正需要數據時才會跑一趟數據庫。
load() 方法 和 get() 方法的基礎區別:
- load() 支持延遲加載(Lazy);
意思是,別急,你需要時我再去拿數據。如果沒有拿到數據,則會拋出異常。
- get() 方法不支持延遲加載,而是(Fetch),如果沒有拿到數據,則返回 null 。
什么時候使用 get(),什么時候使用 load()。只有需求才能告訴你如何權衡,沒有絕對的忠告。
3.2 延遲加載的意義
答案很簡單:錯峰出行,需時索取。
數據庫系統的迎接能力終歸是有限的。面對同時有很多數據請求時,就會造成擁堵。并不是所有的數據請求會在它的邏輯中立即使用數據。
于是,就可以使用延遲加載技術,暫緩數據請求,真正需要時,或錯開數據庫系統的訪問高峰期后再訪問。
在真實的企業級項目中,一個業務邏輯往往是借助于多個組件一起協作完成的。
Hibernate 作為數據請求框架,充當數據提供者角色,本身并不處理數據。數據的使用延遲到了數據加工組件之中。
于是,Hibernate 用不著立即造訪數據庫,先給數據加工組件提供一個代理對象,等數據加工組件真正需要數據時再訪問數據庫也不遲。
延遲加載是 Hibernate 中的性能優化技術,不要誤會它是在使什么小心眼。完全是一番好意。
哲學上講世界是平衡的,一頭變輕,另一頭就會變重??偰芰肯牟蛔儭?/p>
延遲加載技術提供了一種性能優化方式(變輕了),但在還沒有真正獲取數據之前,不能關閉 Session 對象(生命周期延長,變重了)算是平衡制約吧。
4. 小結
本節課聊到了 Hibernate 中一個很重要的概念:延遲加載,是一種性能優化技術。讓開發者在真正需要數據的時候才進入到數據庫。
Session 提供的 load() 方法支持延遲加載。但是,千萬別以為延遲加載僅僅是 load() 方法的專利。
延遲加載是性能優化技術,Hibernate 在設計時,凡是考慮有必要使用的地方都會有延遲加載的身影。