一娩贷、延遲加載簡(jiǎn)介
當(dāng)Hibernate從數(shù)據(jù)庫(kù)中初始化某個(gè)持久態(tài)實(shí)體時(shí)跋选,如果集合屬性里包含十幾萬(wàn)、甚至百萬(wàn)條記錄官脓,在初始化持久態(tài)實(shí)體的同時(shí)协怒,完成所有集合屬性的抓取,將導(dǎo)致性能急劇下降卑笨。有可能系統(tǒng)只需要持久態(tài)實(shí)體集合屬性中的部分記錄而不是全部記錄孕暇,這樣,沒(méi)必要一次加載所有的集合屬性赤兴。
??對(duì)于集合屬性妖滔,通常推薦使用延遲加載策略。所謂延遲加載策略就是等系統(tǒng)需要集合屬性時(shí)才從數(shù)據(jù)庫(kù)裝在關(guān)聯(lián)的數(shù)據(jù)桶良。通過(guò)延遲加載技術(shù)可以避免過(guò)多座舍、過(guò)早地加載數(shù)據(jù)表中的數(shù)據(jù),從而降低內(nèi)存開(kāi)銷(xiāo)陨帆。
二曲秉、延遲加載的本質(zhì)
延遲加載的本質(zhì)上就是代理模式的應(yīng)用,當(dāng)程序通過(guò)Hibernate裝載一個(gè)實(shí)體時(shí)疲牵,默認(rèn)情況下承二,Hibernate并不會(huì)立即抓取它的屬性集合、關(guān)聯(lián)屬性所對(duì)應(yīng)記錄纲爸,而是通過(guò)生成一個(gè)代理來(lái)表示這些集合屬性亥鸠、關(guān)聯(lián)實(shí)體,這個(gè)代理僅僅保存了id這個(gè)屬性识啦。
三负蚊、get和load
在Hibernate中如果我們要從數(shù)據(jù)庫(kù)獲取一個(gè)對(duì)象,通常有兩種方式袁滥,一種是通過(guò)get()盖桥,另一種就是通過(guò)load()。get和load方法都要通過(guò)主鍵進(jìn)行查詢题翻,其他字段不能夠使用揩徊。
(1)get加載方式
??當(dāng)我們使用session.get()方法來(lái)獲取一個(gè)對(duì)象時(shí),不管我們是不是用這個(gè)對(duì)象嵌赠,此時(shí)都會(huì)發(fā)出sql語(yǔ)句去從數(shù)據(jù)庫(kù)中查詢出來(lái)塑荒。
//通過(guò)get方法來(lái)加載對(duì)象時(shí),不管使不使用該對(duì)象姜挺,都會(huì)發(fā)出sql語(yǔ)句齿税,從數(shù)據(jù)庫(kù)中查詢
User user = (User)session.get(User.class, 2);
此時(shí)我們通過(guò)get方式來(lái)得到user對(duì)象,但是我們并沒(méi)有使用它(比如user.getId())炊豪,但是我們發(fā)現(xiàn)控制臺(tái)會(huì)輸出sql的查詢語(yǔ)句:
Hibernate: select user0_.id as id0_0_, user0_.username as username0_0_, user0_.password as password0_0_, user0_.born as born0_0_ from user user0_ where user0_.id=?
(2)load加載方式
??當(dāng)使用load方法來(lái)得到一個(gè)對(duì)象時(shí)凌箕,此時(shí)hibernate會(huì)使用延遲加載的機(jī)制來(lái)加載這個(gè)對(duì)象拧篮,也就是不會(huì)發(fā)出sql語(yǔ)句。load方法得到的對(duì)象其實(shí)是一個(gè)代理對(duì)象牵舱,這個(gè)代理對(duì)象只保存了實(shí)體對(duì)象的id值串绩。只有當(dāng)我們要使用這個(gè)對(duì)象,得到其他屬性時(shí)芜壁,這個(gè)時(shí)候才會(huì)發(fā)出sql語(yǔ)句礁凡,從數(shù)據(jù)庫(kù)中去查詢我們的對(duì)象。
//此時(shí)并不會(huì)發(fā)出sql語(yǔ)句慧妄,只有當(dāng)我們需要使用的時(shí)候才會(huì)從數(shù)據(jù)庫(kù)中去查詢
User user = (User)session.load(User.class, 2);
我們看到顷牌,如果我們僅僅是通過(guò)load來(lái)加載我們的User對(duì)象,此時(shí)從控制臺(tái)我們會(huì)發(fā)現(xiàn)并不會(huì)從數(shù)據(jù)庫(kù)中查詢出該對(duì)象塞淹,即并不會(huì)發(fā)出sql語(yǔ)句窟蓝,但如果我們要使用該對(duì)象時(shí):
session = HibernateUtil.openSession();
User user = (User)session.load(User.class, 2);
System.out.println(user);
此時(shí)我們看到控制臺(tái)會(huì)發(fā)出了sql查詢語(yǔ)句,會(huì)將該對(duì)象從數(shù)據(jù)庫(kù)中查詢出來(lái):
Hibernate: select user0_.id as id0_0_, user0_.username as username0_0_, user0_.password as password0_0_, user0_.born as born0_0_ from user user0_ where user0_.id=?
User [id=2, username=aaa, password=111, born=2013-10-16 00:14:24.0]
這個(gè)時(shí)候我們可能會(huì)想窖铡,那么既然調(diào)用load方法時(shí)疗锐,并不會(huì)發(fā)出sql語(yǔ)句去從數(shù)據(jù)庫(kù)中查出該對(duì)象,那么這個(gè)User對(duì)象到底是個(gè)什么對(duì)象呢费彼?
??答案揭曉滑臊!User對(duì)象是我們的一個(gè)代理對(duì)象,這個(gè)代理對(duì)象僅僅保存了id這個(gè)屬性箍铲。
session = HibernateUtil.openSession();
User user = (User)session.load(User.class, 2);
System.out.println(user.getId());
所以如果我們只打印出這個(gè)user對(duì)象的id值時(shí)雇卷,此時(shí)控制臺(tái)會(huì)打印出該id值,但是同樣不會(huì)發(fā)出sql語(yǔ)句去從數(shù)據(jù)庫(kù)中去查詢颠猴。
(3)get和load方法的區(qū)別
①當(dāng)我們?cè)噲D得到一個(gè)id不存在的對(duì)象時(shí)关划,get和load的返回結(jié)果不同:如果使用get方式來(lái)加載對(duì)象,會(huì)返回null翘瓮,當(dāng)我們使用這個(gè)對(duì)象的時(shí)候就會(huì)拋出NullPointException異常贮折;而如果使用load方式來(lái)加載對(duì)象,會(huì)返回一個(gè)代理對(duì)象资盅,當(dāng)我們使用這個(gè)對(duì)象的時(shí)候會(huì)報(bào)ObjectNotFoundException異常调榄。
??那么這是為什么呢?是因?yàn)間et()方法直接返回實(shí)體類,如果查不到數(shù)據(jù)則返回null呵扛。load()會(huì)返回一個(gè)實(shí)體代理對(duì)象(當(dāng)前這個(gè)對(duì)象可以自動(dòng)轉(zhuǎn)化為實(shí)體對(duì)象)每庆,hibernate認(rèn)為該id對(duì)應(yīng)的對(duì)象(數(shù)據(jù)庫(kù)記錄)在數(shù)據(jù)庫(kù)中是一定存在的,所以它可以放心的使用今穿,它可以放心的使用代理來(lái) 延遲加載該對(duì)象缤灵。在用到對(duì)象中的其他屬性數(shù)據(jù)時(shí)才查詢數(shù)據(jù)庫(kù),但是萬(wàn)一數(shù)據(jù)庫(kù)中不存在該記錄,那沒(méi)辦法腮出,只能拋異常帖鸦。
session = HibernateUtil.openSession();
//返回null,此時(shí)不拋異常
User user = (User)session.get(User.class, 20);
//拋出NullPointException異常
System.out.println(user.getUsername());
session = HibernateUtil.openSession();
//返回代理對(duì)象利诺,此時(shí)不拋異常
User user = (User)session.load(User.class, 20);
//拋出ObjectNotFoundException異常
System.out.println(user.getUsername());
②load方法和get方法查詢對(duì)象的過(guò)程不同富蓄。
??Hibernate維持了兩級(jí)緩存。第一級(jí)緩存是session緩存慢逾,也稱為內(nèi)部緩存,由hibernate管理灭红,其中保存了session當(dāng)前所有關(guān)聯(lián)實(shí)體的數(shù)據(jù)侣滩;第二級(jí)緩存是SessionFactory級(jí)別的全局緩存,它是屬于進(jìn)程范圍或群集范圍的緩存变擒,如Ehcache君珠,這一級(jí)別的緩存可以進(jìn)行配置和更改,并且可以動(dòng)態(tài)加載和卸載娇斑。
??這里我們看一下session加載對(duì)象的過(guò)程:出于性能考慮策添,session在調(diào)用數(shù)據(jù)庫(kù)查詢功能之前,會(huì)先在第一級(jí)緩存中進(jìn)行查詢毫缆。如果第一級(jí)緩存查找命中唯竹,且數(shù)據(jù)狀態(tài)合法,則直接返回苦丁。如果不命中浸颓,session會(huì)在當(dāng)前查詢黑名單列表(記錄了當(dāng)前session實(shí)例在之前所有查詢操作中,未能查詢到有效數(shù)據(jù)的查詢條件)記錄中查找旺拉,如果記錄中存在同樣的查詢條件产上,則返回null(既然查不到就沒(méi)必要查下去了)。對(duì)于load和get方法蛾狗,如果緩存中查找不到數(shù)據(jù)晋涣,則發(fā)起數(shù)據(jù)庫(kù)查詢操作,如經(jīng)過(guò)查詢未發(fā)現(xiàn)對(duì)應(yīng)記錄沉桌,則將此次查詢的信息在“查詢黑名單列表”中加以記錄谢鹊,并返回null。
??然后我們看一下get和load方法加載對(duì)象的過(guò)程:
- get先到緩存(session緩存/二級(jí)緩存)中去查看該id對(duì)應(yīng)的對(duì)象是否存在蒲牧,如果沒(méi)有就到DB中去查(即馬上發(fā)出sql)
- load方法創(chuàng)建時(shí)首先查詢session緩存看看該id對(duì)應(yīng)的對(duì)象是否存在撇贺,沒(méi)有則判斷是否是lazy,如果不是直接訪問(wèn)數(shù)據(jù)庫(kù)檢索,查到記錄返回冰抢,是lazy的話就創(chuàng)建代理(不馬上到DB中去找)松嘶,實(shí)際使用數(shù)據(jù)時(shí)才查詢二級(jí)緩存和數(shù)據(jù)庫(kù)。
??對(duì)于load和get方法返回類型:"get()永遠(yuǎn)只返回實(shí)體類",但實(shí)際上這是不正確的挎扰。get方法如果在session緩存中找到了該id對(duì)應(yīng)的對(duì)象翠订,如果剛好該對(duì)象前面是被代理過(guò)的巢音,如被load方法使用過(guò),或者被其他關(guān)聯(lián)對(duì)象延遲加載過(guò)尽超,那么返回的還是原先的代理對(duì)象官撼,而不是實(shí)體類對(duì)象。如果該代理對(duì)象還沒(méi)有加載實(shí)體數(shù)據(jù)(就是id以外的其他屬性數(shù)據(jù))似谁,那么在用到這個(gè)對(duì)象的時(shí)候傲绣,它會(huì)查詢二級(jí)緩存或者數(shù)據(jù)庫(kù)來(lái)加載數(shù)據(jù)(即不會(huì)立即查詢數(shù)據(jù)庫(kù)或者二級(jí)緩存),但是返回的還是代理對(duì)象巩踏,只不過(guò)已經(jīng)加載了實(shí)體數(shù)據(jù)秃诵。(代理對(duì)象實(shí)際就是空的對(duì)象,并沒(méi)有去數(shù)據(jù)庫(kù)查詢數(shù)據(jù)塞琼;如果去數(shù)據(jù)庫(kù)查詢了 返回到了這個(gè)對(duì)象 菠净,我們叫實(shí)體對(duì)象)
??十分注意,Hibernate雖然允許對(duì)關(guān)聯(lián)對(duì)象彪杉、屬性進(jìn)行延遲加載毅往,但是必須保證延遲加載的操作限于同一個(gè) Hibernate Session范圍之內(nèi)進(jìn)行。同時(shí)要遵守一個(gè)請(qǐng)求一個(gè)Hibernate session的原則派近。
四攀唯、延遲加載和關(guān)聯(lián)查詢
默認(rèn)情況下,Hibernate也會(huì)采用延遲加載來(lái)加載關(guān)聯(lián)實(shí)體构哺,不管是一對(duì)多關(guān)聯(lián)革答、還是一對(duì)一關(guān)聯(lián)、多對(duì)多關(guān)聯(lián)曙强,Hibernate 默認(rèn)都會(huì)采用延遲加載残拐。Hibernate采用“延遲加載”管理關(guān)聯(lián)實(shí)體的模式時(shí),select在查詢時(shí)只會(huì)查出主表記錄碟嘴,用到了關(guān)聯(lián)數(shù)據(jù)時(shí)再自動(dòng)在執(zhí)行查詢溪食,也就是在加載主實(shí)體時(shí),并未真正去抓取關(guān)聯(lián)實(shí)體對(duì)應(yīng)數(shù)據(jù)娜扇,而只是動(dòng)態(tài)地生成一個(gè)對(duì)象作為關(guān)聯(lián)實(shí)體的代理错沃。
五、使用延遲加載帶來(lái)的問(wèn)題
使用延遲加載最經(jīng)常出現(xiàn)的就是LazyInitializationException異常雀瓢。
我們來(lái)看一個(gè)例子:
public class UserDAO
{
public User loadUser(int id)
{
Session session = null;
Transaction tx = null;
User user = null;
try
{
session = HibernateUtil.openSession();
tx = session.beginTransaction();
user = (User)session.load(User.class, 1);
tx.commit();
}
catch (Exception e)
{
e.printStackTrace();
tx.rollback();
}
finally
{
HibernateUtil.close(session);
}
return user;
}
}
@Test
public void testLazy()
{
UserDAO userDAO = new UserDAO();
User user = userDAO.loadUser(2);
System.out.println(user.getName());
}
運(yùn)行測(cè)試用例枢析,控制臺(tái)會(huì)拋出如下錯(cuò)誤:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session .............
產(chǎn)生這個(gè)異常的原因是當(dāng)我們通過(guò)load方法加載一個(gè)對(duì)象時(shí),并沒(méi)有發(fā)出sql語(yǔ)句去從數(shù)據(jù)庫(kù)中查詢出該對(duì)象刃麸,當(dāng)前這個(gè)對(duì)象僅僅是一個(gè)代理對(duì)象醒叁,我們并沒(méi)有使用它,但是此時(shí)我們的session已經(jīng)關(guān)閉了,所以當(dāng)我們?cè)跍y(cè)試用例中使用該對(duì)象時(shí)就會(huì)報(bào)LazyInitializationException這個(gè)異常了把沼。
??解決這個(gè)問(wèn)題的方法有以下幾種:
a.將load改成get的方式來(lái)得到該對(duì)象
??這個(gè)解決辦法相當(dāng)于關(guān)閉延遲加載啊易,但是這樣帶來(lái)的隱患是十分大的。從理論的角度講饮睬,最好是用一個(gè)就關(guān)一個(gè)租谈,防止資源消耗。如果關(guān)閉了延遲加載捆愁,那么在關(guān)聯(lián)查詢中割去,如果關(guān)聯(lián)表越多,則每次查詢的開(kāi)銷(xiāo)就越大昼丑。所以不建議使用這個(gè)方法解決劫拗。
b.使用OpenSessionInViewFilter/OpenSessionInViewInterceptor
??OpenSessionInViewFilter是Spring提供的一個(gè)針對(duì)Hibernate的一個(gè)支持類,其主要意思是在發(fā)起一個(gè)頁(yè)面請(qǐng)求時(shí)打開(kāi)Hibernate的Session矾克,一直保持這個(gè)Session,直到這個(gè)請(qǐng)求結(jié)束憔足,具體是通過(guò)一個(gè)Filter來(lái)實(shí)現(xiàn)的胁附。如果應(yīng)用中使用了OpenSessionInViewFilter或者OpenSessionInViewInterceptor,所有打開(kāi)的 session會(huì)被保存在一個(gè)線程變量里滓彰。在線程退出前通過(guò)OpenSessionInViewFilter或者OpenSessionInViewInterceptor斷開(kāi)這些session控妻。
??在OpenSessionInView的配置中,singleSession應(yīng)該設(shè)置為true揭绑,表示一個(gè)request只能打開(kāi)一個(gè) session(符合Hibernate一個(gè)請(qǐng)求一個(gè)session的原則)弓候,如果設(shè)置為false的話,session可以被打開(kāi)多個(gè)他匪,這時(shí)在update菇存、delete的時(shí)候會(huì)出現(xiàn)打開(kāi)多個(gè)session的異常。
??使用OpenSessionInViewFilter確實(shí)可以降低延遲加載所引發(fā)的的各種問(wèn)題邦蜜,使Service層代碼更易開(kāi)發(fā)和維護(hù)依鸥。但是對(duì)于大型且高并發(fā)的應(yīng)用來(lái)說(shuō),強(qiáng)烈建議不要使用OpenSessionInViewFilter悼沈,因?yàn)镺penSessionInViewFilter會(huì)讓每個(gè)web請(qǐng)求線程都綁定一個(gè)Hibernate的Session贱迟,即會(huì)綁定一個(gè)數(shù)據(jù)連接,直到完成web請(qǐng)求處理時(shí)才釋放數(shù)據(jù)連接絮供。這會(huì)造成兩個(gè)性能問(wèn)題:
- 加大對(duì)數(shù)據(jù)連接資源訪問(wèn)的并發(fā)性(因?yàn)樵居行﹚eb請(qǐng)求可能并不需要使用到數(shù)據(jù)庫(kù)連接)衣吠;
- 延長(zhǎng)了每個(gè)web請(qǐng)求對(duì)數(shù)據(jù)連接資源占用的時(shí)長(zhǎng)。由于OpenSessionInViewFilter把session綁在當(dāng)前線程上壤靶,導(dǎo)致session的生命周期比事務(wù)要長(zhǎng)缚俏,這期間所有事務(wù)性操作都在復(fù)用這同一個(gè)session,由此可能會(huì)產(chǎn)生了一些“怪問(wèn)題”。
??應(yīng)用系統(tǒng)的瓶頸往往都出現(xiàn)在數(shù)據(jù)庫(kù)上袍榆,使用OpenSessionInViewFilter會(huì)使系統(tǒng)更容易出現(xiàn)數(shù)據(jù)庫(kù)資源的性能瓶頸胀屿。OpenSessionInViewFilter調(diào)用流程為:request(請(qǐng)求)->open session并開(kāi)始transaction->controller->View(Jsp)->結(jié)束transaction并close session。我們?cè)囅胂氯绻鞒讨械哪骋徊奖蛔枞脑挵福窃谶@期間connection就一直被占用而不釋 放宿崭。最有可能被阻塞的就是在寫(xiě)Jsp這步,一方面可能是頁(yè)面內(nèi)容大才写,response.write()的時(shí)間長(zhǎng)葡兑,另一方面可能是網(wǎng)速慢,服務(wù)器與用戶間傳輸時(shí) 間久赞草。當(dāng)大量這樣的情況出現(xiàn)時(shí)讹堤,就有連接池連接不足,造成頁(yè)面假死現(xiàn)象厨疙。
c.設(shè)計(jì)好Service接口
??開(kāi)發(fā)者必須要設(shè)計(jì)好Service接口洲守,使延遲加載的工作在Service層內(nèi)完成,也就是盡量在Service下載完所有的東西沾凄。此外梗醇,還要求提供滿足不同需求的Service接口。假設(shè)User和Dept兩個(gè)實(shí)體對(duì)象是Many-to-One的關(guān)系撒蟀,User中的Dept使用了延遲加載方案叙谨,那么UserService 需要提供兩個(gè)不同的獲取User對(duì)象的服務(wù)接口,以應(yīng)對(duì)不同的應(yīng)用場(chǎng)景:其一為getUserWithDept()保屯,返回帶Dept的User手负;其二為getUserWithoutDept(),返回不帶Dept的User姑尺。
d.通過(guò)ThreadLocal來(lái)處理session
private static ThreadLocal<Session> sessionHolder = new ThreadLocal<Session>();
private static void setSession(Session session) {
sessionHolder.set(session);
}
public static Session getSession() {
return sessionHolder.get();
}
private static void removeSession() {
sessionHolder.remove();
}