抓取策略(fetching strategy) 是指:當(dāng)應(yīng)用程序需要在(Hibernate實(shí)體對(duì)象圖的)關(guān)聯(lián)關(guān)系間進(jìn)行導(dǎo)航的時(shí)候, Hibernate如何獲取關(guān)聯(lián)對(duì)象的策略。抓取策略可以在O/R映射的元數(shù)據(jù)中聲明,也可以在特定的HQL 或條件查詢(Criteria Query)中重載聲明。
Hibernate3 定義了如下幾種抓取策略:
-
連接抓取(Join fetching) - Hibernate通過(guò) 在SELECT語(yǔ)句使用OUTER JOIN(外連接)來(lái) 獲得對(duì)象的關(guān)聯(lián)實(shí)例或者關(guān)聯(lián)集合。
-
查詢抓取(Select fetching) - 另外發(fā)送一條 SELECT 語(yǔ)句抓取當(dāng)前對(duì)象的關(guān)聯(lián)實(shí)體或集合。除非你顯式的指定lazy="false"禁止 延遲抓取(lazy fetching),否則只有當(dāng)你真正訪問(wèn)關(guān)聯(lián)關(guān)系的時(shí)候,才會(huì)執(zhí)行第二條select語(yǔ)句。
-
子查詢抓取(Subselect fetching) - 另外發(fā)送一條SELECT 語(yǔ)句抓取在前面查詢到(或者抓取到)的所有實(shí)體對(duì)象的關(guān)聯(lián)集合。除非你顯式的指定lazy="false" 禁止延遲抓取(lazy fetching),否則只有當(dāng)你真正訪問(wèn)關(guān)聯(lián)關(guān)系的時(shí)候,才會(huì)執(zhí)行第二條select語(yǔ)句。
-
批量抓取(Batch fetching) - 對(duì)查詢抓取的優(yōu)化方案, 通過(guò)指定一個(gè)主鍵或外鍵列表,Hibernate使用單條SELECT語(yǔ)句獲取一批對(duì)象實(shí)例或集合。
Hibernate會(huì)區(qū)分下列各種情況:
-
Immediate fetching,立即抓取 - 當(dāng)宿主被加載時(shí),關(guān)聯(lián)、集合或?qū)傩员涣⒓醋ト ?
-
Lazy collection fetching,延遲集合抓取- 直到應(yīng)用程序?qū)线M(jìn)行了一次操作時(shí),集合才被抓取。(對(duì)集合而言這是默認(rèn)行為。)
-
Proxy fetching,代理抓取 - 對(duì)返回單值的關(guān)聯(lián)而言,當(dāng)其某個(gè)方法被調(diào)用,而非對(duì)其關(guān)鍵字進(jìn)行g(shù)et操作時(shí)才抓取。
-
Lazy attribute fetching,屬性延遲加載 - 對(duì)屬性或返回單值的關(guān)聯(lián)而言,當(dāng)其實(shí)例變量被訪問(wèn)的時(shí)候進(jìn)行抓取(需要運(yùn)行時(shí)字節(jié)碼強(qiáng)化)。這一方法很少是必要的。
這里有兩個(gè)正交的概念:關(guān)聯(lián)何時(shí)被抓取,以及被如何抓取(會(huì)采用什么樣的SQL語(yǔ)句)。不要混淆它們!我們使用抓取來(lái)改善性能。我們使用延遲來(lái)定義一些契約,對(duì)某特定類(lèi)的某個(gè)脫管的實(shí)例,知道有哪些數(shù)據(jù)是可以使用的。
默認(rèn)情況下,Hibernate 3對(duì)集合使用延遲select抓取,對(duì)返回單值的關(guān)聯(lián)使用延遲代理抓取。對(duì)幾乎是所有的應(yīng)用而言,其絕大多數(shù)的關(guān)聯(lián),這種策略都是有效的。
注意:假若你設(shè)置了hibernate.default_batch_fetch_size,Hibernate會(huì)對(duì)延遲加載采取批量抓取優(yōu)化措施(這種優(yōu)化也可能會(huì)在更細(xì)化的級(jí)別打開(kāi))。
然而,你必須了解延遲抓取帶來(lái)的一個(gè)問(wèn)題。在一個(gè)打開(kāi)的Hibernate session上下文之外調(diào)用延遲集合會(huì)導(dǎo)致一次意外。比如:
s = sessions.openSession(); Transaction tx = s.beginTransaction(); User u = (User) s.createQuery("from User u where u.name=:userName") .setString("userName", userName).uniqueResult(); Map permissions = u.getPermissions(); tx.commit(); s.close(); Integer accessLevel = (Integer) permissions.get("accounts"); // Error!
在Session關(guān)閉后,permessions集合將是未實(shí)例化的、不再可用,因此無(wú)法正常載入其狀態(tài)。 Hibernate對(duì)脫管對(duì)象不支持延遲實(shí)例化. 這里的修改方法是:將permissions讀取數(shù)據(jù)的代碼 移到tx.commit()之前。
除此之外,通過(guò)對(duì)關(guān)聯(lián)映射指定lazy="false",我們也可以使用非延遲的集合或關(guān)聯(lián)。但是, 對(duì)絕大部分集合來(lái)說(shuō),更推薦使用延遲方式抓取數(shù)據(jù)。如果在你的對(duì)象模型中定義了太多的非延遲關(guān)聯(lián),Hibernate最終幾乎需要在每個(gè)事務(wù)中載入整個(gè)數(shù)據(jù)庫(kù)到內(nèi)存中!
但是,另一方面,在一些特殊的事務(wù)中,我們也經(jīng)常需要使用到連接抓取(它本身上就是非延遲的),以代替查詢抓取。 下面我們將會(huì)很快明白如何具體的定制Hibernate中的抓取策略。在Hibernate3中,具體選擇哪種抓取策略的機(jī)制是和選擇 單值關(guān)聯(lián)或集合關(guān)聯(lián)相一致的。
查詢抓取(默認(rèn)的)在N+1查詢的情況下是極其脆弱的,因此我們可能會(huì)要求在映射文檔中定義使用連接抓取:
<set name="permissions" fetch="join"> <key column="userId"/> <one-to-many class="Permission"/> </set
<many-to-one name="mother" class="Cat" fetch="join"/>
在映射文檔中定義的抓取策略將會(huì)有產(chǎn)生以下影響:
-
通過(guò)get()或load()方法取得數(shù)據(jù)。
-
只有在關(guān)聯(lián)之間進(jìn)行導(dǎo)航時(shí),才會(huì)隱式的取得數(shù)據(jù)(延遲抓取)。
-
條件查詢
通常情況下,我們并不使用映射文檔進(jìn)行抓取策略的定制。更多的是,保持其默認(rèn)值,然后在特定的事務(wù)中, 使用HQL的左連接抓取(left join fetch) 對(duì)其進(jìn)行重載。這將通知 Hibernate在第一次查詢中使用外部關(guān)聯(lián)(outer join),直接得到其關(guān)聯(lián)數(shù)據(jù)。 在條件查詢 API中,應(yīng)該調(diào)用 setFetchMode(FetchMode.JOIN)語(yǔ)句。
也許你喜歡僅僅通過(guò)條件查詢,就可以改變get() 或 load()語(yǔ)句中的數(shù)據(jù)抓取策略。例如:
User user = (User) session.createCriteria(User.class) .setFetchMode("permissions", FetchMode.JOIN) .add( Restrictions.idEq(userId) ) .uniqueResult();
(這就是其他ORM解決方案的“抓取計(jì)劃(fetch plan)”在Hibernate中的等價(jià)物。)
截然不同的一種避免N+1次查詢的方法是,使用二級(jí)緩存。
在Hinerbate中,對(duì)集合的延遲抓取的采用了自己的實(shí)現(xiàn)方法。但是,對(duì)于單端關(guān)聯(lián)的延遲抓取,則需要采用 其他不同的機(jī)制。單端關(guān)聯(lián)的目標(biāo)實(shí)體必須使用代理,Hihernate在運(yùn)行期二進(jìn)制級(jí)(通過(guò)優(yōu)異的CGLIB庫(kù)), 為持久對(duì)象實(shí)現(xiàn)了延遲載入代理。
默認(rèn)的,Hibernate3將會(huì)為所有的持久對(duì)象產(chǎn)生代理(在啟動(dòng)階段),然后使用他們實(shí)現(xiàn) 多對(duì)一(many-to-one)關(guān)聯(lián)和一對(duì)一(one-to-one) 關(guān)聯(lián)的延遲抓取。
在映射文件中,可以通過(guò)設(shè)置proxy屬性為目標(biāo)class聲明一個(gè)接口供代理接口使用。 默認(rèn)的,Hibernate將會(huì)使用該類(lèi)的一個(gè)子類(lèi)。 注意:被代理的類(lèi)必須實(shí)現(xiàn)一個(gè)至少包可見(jiàn)的默認(rèn)構(gòu)造函數(shù),我們建議所有的持久類(lèi)都應(yīng)擁有這樣的構(gòu)造函數(shù)
在如此方式定義一個(gè)多態(tài)類(lèi)的時(shí)候,有許多值得注意的常見(jiàn)性的問(wèn)題,例如:
<class name="Cat" proxy="Cat"> ...... <subclass name="DomesticCat"> ..... </subclass> </class>
首先,Cat實(shí)例永遠(yuǎn)不可以被強(qiáng)制轉(zhuǎn)換為DomesticCat, 即使它本身就是DomesticCat實(shí)例。
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a proxy (does not hit the db) if ( cat.isDomesticCat() ) { // hit the db to initialize the proxy DomesticCat dc = (DomesticCat) cat; // Error! .... }
其次,代理的“==”可能不再成立。
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a Cat proxy DomesticCat dc = (DomesticCat) session.load(DomesticCat.class, id); // acquire new DomesticCat proxy! System.out.println(cat==dc); // false
雖然如此,但實(shí)際情況并沒(méi)有看上去那么糟糕。雖然我們現(xiàn)在有兩個(gè)不同的引用,分別指向這兩個(gè)不同的代理對(duì)象, 但實(shí)際上,其底層應(yīng)該是同一個(gè)實(shí)例對(duì)象:
cat.setWeight(11.0); // hit the db to initialize the proxy System.out.println( dc.getWeight() ); // 11.0
第三,你不能對(duì)“final類(lèi)”或“具有final方法的類(lèi)”使用CGLIB代理。
最后,如果你的持久化對(duì)象在實(shí)例化時(shí)需要某些資源(例如,在實(shí)例化方法、默認(rèn)構(gòu)造方法中), 那么代理對(duì)象也同樣需要使用這些資源。實(shí)際上,代理類(lèi)是持久化類(lèi)的子類(lèi)。
這些問(wèn)題都源于Java的單根繼承模型的天生限制。如果你希望避免這些問(wèn)題,那么你的每個(gè)持久化類(lèi)必須實(shí)現(xiàn)一個(gè)接口, 在此接口中已經(jīng)聲明了其業(yè)務(wù)方法。然后,你需要在映射文檔中再指定這些接口。例如:
<class name="CatImpl" proxy="Cat"> ...... <subclass name="DomesticCatImpl" proxy="DomesticCat"> ..... </subclass> </class>
這里CatImpl實(shí)現(xiàn)了Cat接口, DomesticCatImpl實(shí)現(xiàn)DomesticCat接口。 在load()、iterate()方法中就會(huì)返回 Cat和DomesticCat的代理對(duì)象。 (注意list()并不會(huì)返回代理對(duì)象。)
Cat cat = (Cat) session.load(CatImpl.class, catid); Iterator iter = session.iterate("from CatImpl as cat where cat.name='fritz'"); Cat fritz = (Cat) iter.next();
這里,對(duì)象之間的關(guān)系也將被延遲載入。這就意味著,你應(yīng)該將屬性聲明為Cat,而不是CatImpl。
但是,在有些方法中是不需要使用代理的。例如:
-
equals()方法,如果持久類(lèi)沒(méi)有重載equals()方法。
-
hashCode()方法,如果持久類(lèi)沒(méi)有重載hashCode()方法。
-
標(biāo)志符的getter方法。
Hibernate將會(huì)識(shí)別出那些重載了equals()、或hashCode()方法的持久化類(lèi)。
在Session范圍之外訪問(wèn)未初始化的集合或代理,Hibernate將會(huì)拋出LazyInitializationException異常。 也就是說(shuō),在分離狀態(tài)下,訪問(wèn)一個(gè)實(shí)體所擁有的集合,或者訪問(wèn)其指向代理的屬性時(shí),會(huì)引發(fā)此異常。
有時(shí)候我們需要保證某個(gè)代理或者集合在Session關(guān)閉前就已經(jīng)被初始化了。 當(dāng)然,我們可以通過(guò)強(qiáng)行調(diào)用cat.getSex()或者cat.getKittens().size()之類(lèi)的方法來(lái)確保這一點(diǎn)。 但是這樣的程序會(huì)造成讀者的疑惑,也不符合通常的代碼規(guī)范。
靜態(tài)方法Hibernate.initialized() 為你的應(yīng)用程序提供了一個(gè)便捷的途徑來(lái)延遲加載集合或代理。 只要它的Session處于open狀態(tài),Hibernate.initialize(cat) 將會(huì)為cat強(qiáng)制對(duì)代理實(shí)例化。 同樣,Hibernate.initialize( cat.getKittens() ) 對(duì)kittens的集合具有同樣的功能。
還有另外一種選擇,就是保持Session一直處于open狀態(tài),直到所有需要的集合或代理都被載入。 在某些應(yīng)用架構(gòu)中,特別是對(duì)于那些使用Hibernate進(jìn)行數(shù)據(jù)訪問(wèn)的代碼,以及那些在不同應(yīng)用層和不同物理進(jìn)程中使用Hibernate的代碼。 在集合實(shí)例化時(shí),如何保證Session處于open狀態(tài)經(jīng)常會(huì)是一個(gè)問(wèn)題。有兩種方法可以解決此問(wèn)題:
-
在一個(gè)基于Web的應(yīng)用中,可以利用servlet過(guò)濾器(filter),在用戶請(qǐng)求(request)結(jié)束、頁(yè)面生成 結(jié)束時(shí)關(guān)閉Session(這里使用了在展示層保持打開(kāi)Session模式(Open Session in View)), 當(dāng)然,這將依賴于應(yīng)用框架中異常需要被正確的處理。在返回界面給用戶之前,乃至在生成界面過(guò)程中發(fā)生異常的情況下, 正確關(guān)閉Session和結(jié)束事務(wù)將是非常重要的, Servlet過(guò)濾器必須如此訪問(wèn)Session,才能保證正確使用Session。 我們推薦使用ThreadLocal 變量保存當(dāng)前的Session (可以參考第 1.4 節(jié) “與Cat同樂(lè)”的例子實(shí)現(xiàn))。
-
在一個(gè)擁有單獨(dú)業(yè)務(wù)層的應(yīng)用中,業(yè)務(wù)層必須在返回之前,為web層“準(zhǔn)備”好其所需的數(shù)據(jù)集合。這就意味著 業(yè)務(wù)層應(yīng)該載入所有表現(xiàn)層/web層所需的數(shù)據(jù),并將這些已實(shí)例化完畢的數(shù)據(jù)返回。通常,應(yīng)用程序應(yīng)該 為web層所需的每個(gè)集合調(diào)用Hibernate.initialize()(這個(gè)調(diào)用必須發(fā)生咱session關(guān)閉之前); 或者使用帶有FETCH從句,或FetchMode.JOIN的Hibernate查詢, 事先取得所有的數(shù)據(jù)集合。如果你在應(yīng)用中使用了Command模式,代替Session Facade , 那么這項(xiàng)任務(wù)將會(huì)變得簡(jiǎn)單的多。
-
你也可以通過(guò)merge()或lock()方法,在訪問(wèn)未實(shí)例化的集合(或代理)之前, 為先前載入的對(duì)象綁定一個(gè)新的Session。 顯然,Hibernate將不會(huì),也不應(yīng)該自動(dòng)完成這些任務(wù),因?yàn)檫@將引入一個(gè)特殊的事務(wù)語(yǔ)義。
有時(shí)候,你并不需要完全實(shí)例化整個(gè)大的集合,僅需要了解它的部分信息(例如其大小)、或者集合的部分內(nèi)容。
你可以使用集合過(guò)濾器得到其集合的大小,而不必實(shí)例化整個(gè)集合:
( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()
這里的createFilter()方法也可以被用來(lái)有效的抓取集合的部分內(nèi)容,而無(wú)需實(shí)例化整個(gè)集合:
s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();
Hibernate可以充分有效的使用批量抓取,也就是說(shuō),如果僅一個(gè)訪問(wèn)代理(或集合),那么Hibernate將不載入其他未實(shí)例化的代理。 批量抓取是延遲查詢抓取的優(yōu)化方案,你可以在兩種批量抓取方案之間進(jìn)行選擇:在類(lèi)級(jí)別和集合級(jí)別。
類(lèi)/實(shí)體級(jí)別的批量抓取很容易理解。假設(shè)你在運(yùn)行時(shí)將需要面對(duì)下面的問(wèn)題:你在一個(gè)Session中載入了25個(gè) Cat實(shí)例,每個(gè)Cat實(shí)例都擁有一個(gè)引用成員owner, 其指向Person,而Person類(lèi)是代理,同時(shí)lazy="true"。 如果你必須遍歷整個(gè)cats集合,對(duì)每個(gè)元素調(diào)用getOwner()方法,Hibernate將會(huì)默認(rèn)的執(zhí)行25次SELECT查詢, 得到其owner的代理對(duì)象。這時(shí),你可以通過(guò)在映射文件的Person屬性,顯式聲明batch-size,改變其行為:
<class name="Person" batch-size="10">...</class>
隨之,Hibernate將只需要執(zhí)行三次查詢,分別為10、10、 5。
你也可以在集合級(jí)別定義批量抓取。例如,如果每個(gè)Person都擁有一個(gè)延遲載入的Cats集合, 現(xiàn)在,Sesssion中載入了10個(gè)person對(duì)象,遍歷person集合將會(huì)引起10次SELECT查詢, 每次查詢都會(huì)調(diào)用getCats()方法。如果你在Person的映射定義部分,允許對(duì)cats批量抓取, 那么,Hibernate將可以預(yù)先抓取整個(gè)集合。請(qǐng)看例子:
<class name="Person"> <set name="cats" batch-size="3"> ... </set> </class>
如果整個(gè)的batch-size是3(筆誤?),那么Hibernate將會(huì)分四次執(zhí)行SELECT查詢, 按照3、3、3、1的大小分別載入數(shù)據(jù)。這里的每次載入的數(shù)據(jù)量還具體依賴于當(dāng)前Session中未實(shí)例化集合的個(gè)數(shù)。
如果你的模型中有嵌套的樹(shù)狀結(jié)構(gòu),例如典型的帳單-原料結(jié)構(gòu)(bill-of-materials pattern),集合的批量抓取是非常有用的。 (盡管在更多情況下對(duì)樹(shù)進(jìn)行讀取時(shí),嵌套集合(nested set)或原料路徑(materialized path)(××) 是更好的解決方法。)
假若一個(gè)延遲集合或單值代理需要抓取,Hibernate會(huì)使用一個(gè)subselect重新運(yùn)行原來(lái)的查詢,一次性讀入所有的實(shí)例。這和批量抓取的實(shí)現(xiàn)方法是一樣的,不會(huì)有破碎的加載。
Hibernate3對(duì)單獨(dú)的屬性支持延遲抓取,這項(xiàng)優(yōu)化技術(shù)也被稱為組抓取(fetch groups)。 請(qǐng)注意,該技術(shù)更多的屬于市場(chǎng)特性。在實(shí)際應(yīng)用中,優(yōu)化行讀取比優(yōu)化列讀取更重要。但是,僅載入類(lèi)的部分屬性在某些特定情況下會(huì)有用,例如在原有表中擁有幾百列數(shù)據(jù)、數(shù)據(jù)模型無(wú)法改動(dòng)的情況下。
可以在映射文件中對(duì)特定的屬性設(shè)置lazy,定義該屬性為延遲載入。
<class name="Document"> <id name="id"> <generator class="native"/> </id> <property name="name" not-null="true" length="50"/> <property name="summary" not-null="true" length="200" lazy="true"/> <property name="text" not-null="true" length="2000" lazy="true"/> </class>
屬性的延遲載入要求在其代碼構(gòu)建時(shí)加入二進(jìn)制指示指令(bytecode instrumentation),如果你的持久類(lèi)代碼中未含有這些指令, Hibernate將會(huì)忽略這些屬性的延遲設(shè)置,仍然將其直接載入。
你可以在Ant的Task中,進(jìn)行如下定義,對(duì)持久類(lèi)代碼加入“二進(jìn)制指令。”
<target name="instrument" depends="compile"> <taskdef name="instrument" classname="org.hibernate.tool.instrument.InstrumentTask"> <classpath path="${jar.path}"/> <classpath path="${classes.dir}"/> <classpath refid="lib.class.path"/> </taskdef> <instrument verbose="true"> <fileset dir="${testclasses.dir}/org/hibernate/auction/model"> <include name="*.class"/> </fileset> </instrument> </target>
還有一種可以優(yōu)化的方法,它使用HQL或條件查詢的投影(projection)特性,可以避免讀取非必要的列, 這一點(diǎn)至少對(duì)只讀事務(wù)是非常有用的。它無(wú)需在代碼構(gòu)建時(shí)“二進(jìn)制指令”處理,因此是一個(gè)更加值得選擇的解決方法。
有時(shí)你需要在HQL中通過(guò)抓取所有屬性,強(qiáng)行抓取所有內(nèi)容。
Hibernate的Session在事務(wù)級(jí)別進(jìn)行持久化數(shù)據(jù)的緩存操作。 當(dāng)然,也有可能分別為每個(gè)類(lèi)(或集合),配置集群、或JVM級(jí)別(SessionFactory級(jí)別)的緩存。 你甚至可以為之插入一個(gè)集群的緩存。注意,緩存永遠(yuǎn)不知道其他應(yīng)用程序?qū)Τ志没瘋}(cāng)庫(kù)(數(shù)據(jù)庫(kù))可能進(jìn)行的修改 (即使可以將緩存數(shù)據(jù)設(shè)定為定期失效)。
默認(rèn)情況下,Hibernate使用EHCache進(jìn)行JVM級(jí)別的緩存(目前,Hibernate已經(jīng)廢棄了對(duì)JCS的支持,未來(lái)版本中將會(huì)去掉它)。 你可以通過(guò)設(shè)置hibernate.cache.provider_class屬性,指定其他的緩存策略, 該緩存策略必須實(shí)現(xiàn)org.hibernate.cache.CacheProvider接口。
表 20.1. 緩存策略提供商(Cache Providers)
Cache | Provider class | Type | Cluster Safe | Query Cache Supported |
---|---|---|---|---|
Hashtable (not intended for production use) | org.hibernate.cache.HashtableCacheProvider | memory | yes | |
EHCache | org.hibernate.cache.EhCacheProvider | memory, disk | yes | |
OSCache | org.hibernate.cache.OSCacheProvider | memory, disk | yes | |
SwarmCache | org.hibernate.cache.SwarmCacheProvider | clustered (ip multicast) | yes (clustered invalidation) | |
JBoss TreeCache | org.hibernate.cache.TreeCacheProvider | clustered (ip multicast), transactional | yes (replication) | yes (clock sync req.) |
類(lèi)或者集合映射的“<cache>元素”可以有下列形式:
<cache
usage="transactional|read-write|nonstrict-read-write|read-only" (1)
/>
(1) |
usage說(shuō)明了緩存的策略: transactional、 read-write、 nonstrict-read-write或 read-only。 |
另外(首選?), 你可以在hibernate.cfg.xml中指定<class-cache>和 <collection-cache> 元素。
這里的usage 屬性指明了緩存并發(fā)策略(cache concurrency strategy)。
如果你的應(yīng)用程序只需讀取一個(gè)持久化類(lèi)的實(shí)例,而無(wú)需對(duì)其修改, 那么就可以對(duì)其進(jìn)行只讀 緩存。這是最簡(jiǎn)單,也是實(shí)用性最好的方法。甚至在集群中,它也能完美地運(yùn)作。
<class name="eg.Immutable" mutable="false"> <cache usage="read-only"/> .... </class>
如果應(yīng)用程序需要更新數(shù)據(jù),那么使用讀/寫(xiě)緩存 比較合適。 如果應(yīng)用程序要求“序列化事務(wù)”的隔離級(jí)別(serializable transaction isolation level),那么就決不能使用這種緩存策略。 如果在JTA環(huán)境中使用緩存,你必須指定hibernate.transaction.manager_lookup_class屬性的值, 通過(guò)它,Hibernate才能知道該應(yīng)用程序中JTA的TransactionManager的具體策略。 在其它環(huán)境中,你必須保證在Session.close()、或Session.disconnect()調(diào)用前, 整個(gè)事務(wù)已經(jīng)結(jié)束。 如果你想在集群環(huán)境中使用此策略,你必須保證底層的緩存實(shí)現(xiàn)支持鎖定(locking)。Hibernate內(nèi)置的緩存策略并不支持鎖定功能。
<class name="eg.Cat" .... > <cache usage="read-write"/> .... <set name="kittens" ... > <cache usage="read-write"/> .... </set> </class>
如果應(yīng)用程序只偶爾需要更新數(shù)據(jù)(也就是說(shuō),兩個(gè)事務(wù)同時(shí)更新同一記錄的情況很不常見(jiàn)),也不需要十分嚴(yán)格的事務(wù)隔離, 那么比較適合使用非嚴(yán)格讀/寫(xiě)緩存策略。如果在JTA環(huán)境中使用該策略, 你必須為其指定hibernate.transaction.manager_lookup_class屬性的值, 在其它環(huán)境中,你必須保證在Session.close()、或Session.disconnect()調(diào)用前, 整個(gè)事務(wù)已經(jīng)結(jié)束。
Hibernate的事務(wù)緩存策略提供了全事務(wù)的緩存支持, 例如對(duì)JBoss TreeCache的支持。這樣的緩存只能用于JTA環(huán)境中,你必須指定 為其hibernate.transaction.manager_lookup_class屬性。
沒(méi)有一種緩存提供商能夠支持上列的所有緩存并發(fā)策略。下表中列出了各種提供器、及其各自適用的并發(fā)策略。
無(wú)論何時(shí),當(dāng)你給save()、update()或 saveOrUpdate()方法傳遞一個(gè)對(duì)象時(shí),或使用load()、 get()、list()、iterate() 或scroll()方法獲得一個(gè)對(duì)象時(shí), 該對(duì)象都將被加入到Session的內(nèi)部緩存中。
當(dāng)隨后flush()方法被調(diào)用時(shí),對(duì)象的狀態(tài)會(huì)和數(shù)據(jù)庫(kù)取得同步。 如果你不希望此同步操作發(fā)生,或者你正處理大量對(duì)象、需要對(duì)有效管理內(nèi)存時(shí),你可以調(diào)用evict() 方法,從一級(jí)緩存中去掉這些對(duì)象及其集合。
ScrollableResult cats = sess.createQuery("from Cat as cat").scroll(); //a huge result set while ( cats.next() ) { Cat cat = (Cat) cats.get(0); doSomethingWithACat(cat); sess.evict(cat); }
Session還提供了一個(gè)contains()方法,用來(lái)判斷某個(gè)實(shí)例是否處于當(dāng)前session的緩存中。
如若要把所有的對(duì)象從session緩存中徹底清除,則需要調(diào)用Session.clear()。
對(duì)于二級(jí)緩存來(lái)說(shuō),在SessionFactory中定義了許多方法, 清除緩存中實(shí)例、整個(gè)類(lèi)、集合實(shí)例或者整個(gè)集合。
sessionFactory.evict(Cat.class, catId); //evict a particular Cat sessionFactory.evict(Cat.class); //evict all Cats sessionFactory.evictCollection("Cat.kittens", catId); //evict a particular collection of kittens sessionFactory.evictCollection("Cat.kittens"); //evict all kitten collections
CacheMode參數(shù)用于控制具體的Session如何與二級(jí)緩存進(jìn)行交互。
-
CacheMode.NORMAL - 從二級(jí)緩存中讀、寫(xiě)數(shù)據(jù)。
-
CacheMode.GET - 從二級(jí)緩存中讀取數(shù)據(jù),僅在數(shù)據(jù)更新時(shí)對(duì)二級(jí)緩存寫(xiě)數(shù)據(jù)。
-
CacheMode.PUT - 僅向二級(jí)緩存寫(xiě)數(shù)據(jù),但不從二級(jí)緩存中讀數(shù)據(jù)。
-
CacheMode.REFRESH - 僅向二級(jí)緩存寫(xiě)數(shù)據(jù),但不從二級(jí)緩存中讀數(shù)據(jù)。通過(guò) hibernate.cache.use_minimal_puts的設(shè)置,強(qiáng)制二級(jí)緩存從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù),刷新緩存內(nèi)容。
如若需要查看二級(jí)緩存或查詢緩存區(qū)域的內(nèi)容,你可以使用統(tǒng)計(jì)(Statistics) API。
Map cacheEntries = sessionFactory.getStatistics() .getSecondLevelCacheStatistics(regionName) .getEntries();
此時(shí),你必須手工打開(kāi)統(tǒng)計(jì)選項(xiàng)。可選的,你可以讓Hibernate更人工可讀的方式維護(hù)緩存內(nèi)容。
hibernate.generate_statistics true hibernate.cache.use_structured_entries true
查詢的結(jié)果集也可以被緩存。只有當(dāng)經(jīng)常使用同樣的參數(shù)進(jìn)行查詢時(shí),這才會(huì)有些用處。 要使用查詢緩存,首先你必須打開(kāi)它:
hibernate.cache.use_query_cache true
該設(shè)置將會(huì)創(chuàng)建兩個(gè)緩存區(qū)域 - 一個(gè)用于保存查詢結(jié)果集(org.hibernate.cache.StandardQueryCache); 另一個(gè)則用于保存最近查詢的一系列表的時(shí)間戳(org.hibernate.cache.UpdateTimestampsCache)。 請(qǐng)注意:在查詢緩存中,它并不緩存結(jié)果集中所包含的實(shí)體的確切狀態(tài);它只緩存這些實(shí)體的標(biāo)識(shí)符屬性的值、以及各值類(lèi)型的結(jié)果。 所以查詢緩存通常會(huì)和二級(jí)緩存一起使用。
絕大多數(shù)的查詢并不能從查詢緩存中受益,所以Hibernate默認(rèn)是不進(jìn)行查詢緩存的。如若需要進(jìn)行緩存,請(qǐng)調(diào)用 Query.setCacheable(true)方法。這個(gè)調(diào)用會(huì)讓查詢?cè)趫?zhí)行過(guò)程中時(shí)先從緩存中查找結(jié)果, 并將自己的結(jié)果集放到緩存中去。
如果你要對(duì)查詢緩存的失效政策進(jìn)行精確的控制,你必須調(diào)用Query.setCacheRegion()方法, 為每個(gè)查詢指定其命名的緩存區(qū)域。
List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger") .setEntity("blogger", blogger) .setMaxResults(15) .setCacheable(true) .setCacheRegion("frontpages") .list();
如果查詢需要強(qiáng)行刷新其查詢緩存區(qū)域,那么你應(yīng)該調(diào)用Query.setCacheMode(CacheMode.REFRESH)方法。 這對(duì)在其他進(jìn)程中修改底層數(shù)據(jù)(例如,不通過(guò)Hibernate修改數(shù)據(jù)),或?qū)δ切┬枰x擇性更新特定查詢結(jié)果集的情況特別有用。 這是對(duì)SessionFactory.evictQueries()的更為有效的替代方案,同樣可以清除查詢緩存區(qū)域。
前面我們已經(jīng)對(duì)集合進(jìn)行了足夠的討論。本段中,我們將著重講述集合在運(yùn)行時(shí)的事宜。
Hibernate定義了三種基本類(lèi)型的集合:
-
值數(shù)據(jù)集合
-
一對(duì)多關(guān)聯(lián)
-
多對(duì)多關(guān)聯(lián)
這個(gè)分類(lèi)是區(qū)分了不同的表和外鍵關(guān)系類(lèi)型,但是它沒(méi)有告訴我們關(guān)系模型的所有內(nèi)容。 要完全理解他們的關(guān)系結(jié)構(gòu)和性能特點(diǎn),我們必須同時(shí)考慮“用于Hibernate更新或刪除集合行數(shù)據(jù)的主鍵的結(jié)構(gòu)”。 因此得到了如下的分類(lèi):
-
有序集合類(lèi)
-
集合(sets)
-
包(bags)
所有的有序集合類(lèi)(maps, lists, arrays)都擁有一個(gè)由<key>和 <index>組成的主鍵。 這種情況下集合類(lèi)的更新是非常高效的——主鍵已經(jīng)被有效的索引,因此當(dāng)Hibernate試圖更新或刪除一行時(shí),可以迅速找到該行數(shù)據(jù)。
集合(sets)的主鍵由<key>和其他元素字段構(gòu)成。 對(duì)于有些元素類(lèi)型來(lái)說(shuō),這很低效,特別是組合元素或者大文本、大二進(jìn)制字段; 數(shù)據(jù)庫(kù)可能無(wú)法有效的對(duì)復(fù)雜的主鍵進(jìn)行索引。 另一方面,對(duì)于一對(duì)多、多對(duì)多關(guān)聯(lián),特別是合成的標(biāo)識(shí)符來(lái)說(shuō),集合也可以達(dá)到同樣的高效性能。( 附注:如果你希望SchemaExport為你的<set>創(chuàng)建主鍵, 你必須把所有的字段都聲明為not-null="true"。)
<idbag>映射定義了代理鍵,因此它總是可以很高效的被更新。事實(shí)上, <idbag>擁有著最好的性能表現(xiàn)。
Bag是最差的。因?yàn)閎ag允許重復(fù)的元素值,也沒(méi)有索引字段,因此不可能定義主鍵。 Hibernate無(wú)法判斷出重復(fù)的行。當(dāng)這種集合被更改時(shí),Hibernate將會(huì)先完整地移除 (通過(guò)一個(gè)(in a single DELETE))整個(gè)集合,然后再重新創(chuàng)建整個(gè)集合。 因此Bag是非常低效的。
請(qǐng)注意:對(duì)于一對(duì)多關(guān)聯(lián)來(lái)說(shuō),“主鍵”很可能并不是數(shù)據(jù)庫(kù)表的物理主鍵。 但就算在此情況下,上面的分類(lèi)仍然是有用的。(它仍然反映了Hibernate在集合的各數(shù)據(jù)行中是如何進(jìn)行“定位”的。)
根據(jù)我們上面的討論,顯然有序集合類(lèi)型和大多數(shù)set都可以在增加、刪除、修改元素中擁有最好的性能。
可論證的是對(duì)于多對(duì)多關(guān)聯(lián)、值數(shù)據(jù)集合而言,有序集合類(lèi)比集合(set)有一個(gè)好處。因?yàn)?tt class=literal>Set的內(nèi)在結(jié)構(gòu), 如果“改變”了一個(gè)元素,Hibernate并不會(huì)更新(UPDATE)這一行。 對(duì)于Set來(lái)說(shuō),只有在插入(INSERT)和刪除(DELETE) 操作時(shí)“改變”才有效。再次強(qiáng)調(diào):這段討論對(duì)“一對(duì)多關(guān)聯(lián)”并不適用。
注意到數(shù)組無(wú)法延遲載入,我們可以得出結(jié)論,list, map和idbags是最高效的(非反向)集合類(lèi)型,set則緊隨其后。 在Hibernate中,set應(yīng)該時(shí)最通用的集合類(lèi)型,這時(shí)因?yàn)?#8220;set”的語(yǔ)義在關(guān)系模型中是最自然的。
但是,在設(shè)計(jì)良好的Hibernate領(lǐng)域模型中,我們通常可以看到更多的集合事實(shí)上是帶有inverse="true" 的一對(duì)多的關(guān)聯(lián)。對(duì)于這些關(guān)聯(lián),更新操作將會(huì)在多對(duì)一的這一端進(jìn)行處理。因此對(duì)于此類(lèi)情況,無(wú)需考慮其集合的更新性能。
在把bag扔進(jìn)水溝之前,你必須了解,在一種情況下,bag的性能(包括list)要比set高得多: 對(duì)于指明了inverse="true"的集合類(lèi)(比如說(shuō),標(biāo)準(zhǔn)的雙向的一對(duì)多關(guān)聯(lián)), 我們可以在未初始化(fetch)包元素的情況下直接向bag或list添加新元素! 這是因?yàn)?tt class=literal>Collection.add())或者Collection.addAll() 方法 對(duì)bag或者List總是返回true(這點(diǎn)與與Set不同)。因此對(duì)于下面的相同代碼來(lái)說(shuō),速度會(huì)快得多。
Parent p = (Parent) sess.load(Parent.class, id); Child c = new Child(); c.setParent(p); p.getChildren().add(c); //no need to fetch the collection! sess.flush();
偶爾的,逐個(gè)刪除集合類(lèi)中的元素是相當(dāng)?shù)托У摹ibernate并沒(méi)那么笨, 如果你想要把整個(gè)集合都刪除(比如說(shuō)調(diào)用list.clear()),Hibernate只需要一個(gè)DELETE就搞定了。
假設(shè)我們?cè)谝粋€(gè)長(zhǎng)度為20的集合類(lèi)中新增加了一個(gè)元素,然后再刪除兩個(gè)。 Hibernate會(huì)安排一條INSERT語(yǔ)句和兩條DELETE語(yǔ)句(除非集合類(lèi)是一個(gè)bag)。 這當(dāng)然是顯而易見(jiàn)的。
但是,假設(shè)我們刪除了18個(gè)數(shù)據(jù),只剩下2個(gè),然后新增3個(gè)。則有兩種處理方式:
-
逐一的刪除這18個(gè)數(shù)據(jù),再新增三個(gè);
-
刪除整個(gè)集合類(lèi)(只用一句DELETE語(yǔ)句),然后增加5個(gè)數(shù)據(jù)。
Hibernate還沒(méi)那么聰明,知道第二種選擇可能會(huì)比較快。 (也許讓Hibernate不這么聰明也是好事,否則可能會(huì)引發(fā)意外的“數(shù)據(jù)庫(kù)觸發(fā)器”之類(lèi)的問(wèn)題。)
幸運(yùn)的是,你可以強(qiáng)制使用第二種策略。你需要取消原來(lái)的整個(gè)集合類(lèi)(解除其引用), 然后再返回一個(gè)新的實(shí)例化的集合類(lèi),只包含需要的元素。有些時(shí)候這是非常有用的。
顯然,一次性刪除并不適用于被映射為inverse="true"的集合。
沒(méi)有監(jiān)測(cè)和性能參數(shù)而進(jìn)行優(yōu)化是毫無(wú)意義的。Hibernate為其內(nèi)部操作提供了一系列的示意圖,因此可以從 每個(gè)SessionFactory抓取其統(tǒng)計(jì)數(shù)據(jù)。
你可以有兩種方式訪問(wèn)SessionFactory的數(shù)據(jù)記錄,第一種就是自己直接調(diào)用 sessionFactory.getStatistics()方法讀取、顯示統(tǒng)計(jì)數(shù)據(jù)。
此外,如果你打開(kāi)StatisticsService MBean選項(xiàng),那么Hibernate則可以使用JMX技術(shù) 發(fā)布其數(shù)據(jù)記錄。你可以讓?xiě)?yīng)用中所有的SessionFactory同時(shí)共享一個(gè)MBean,也可以每個(gè) SessionFactory分配一個(gè)MBean。下面的代碼即是其演示代碼:
// MBean service registration for a specific SessionFactory Hashtable tb = new Hashtable(); tb.put("type", "statistics"); tb.put("sessionFactory", "myFinancialApp"); ObjectName on = new ObjectName("hibernate", tb); // MBean object name StatisticsService stats = new StatisticsService(); // MBean implementation stats.setSessionFactory(sessionFactory); // Bind the stats to a SessionFactory server.registerMBean(stats, on); // Register the Mbean on the server
// MBean service registration for all SessionFactory's Hashtable tb = new Hashtable(); tb.put("type", "statistics"); tb.put("sessionFactory", "all"); ObjectName on = new ObjectName("hibernate", tb); // MBean object name StatisticsService stats = new StatisticsService(); // MBean implementation server.registerMBean(stats, on); // Register the MBean on the server
TODO:仍需要說(shuō)明的是:在第一個(gè)例子中,我們直接得到和使用MBean;而在第二個(gè)例子中,在使用MBean之前 我們則需要給出SessionFactory的JNDI名,使用hibernateStatsBean.setSessionFactoryJNDIName("my/JNDI/Name") 得到SessionFactory,然后將MBean保存于其中。
你可以通過(guò)以下方法打開(kāi)或關(guān)閉SessionFactory的監(jiān)測(cè)功能:
-
在配置期間,將hibernate.generate_statistics設(shè)置為true或false;
-
在運(yùn)行期間,則可以可以通過(guò)sf.getStatistics().setStatisticsEnabled(true) 或hibernateStatsBean.setStatisticsEnabled(true)
你也可以在程序中調(diào)用clear()方法重置統(tǒng)計(jì)數(shù)據(jù),調(diào)用logSummary() 在日志中記錄(info級(jí)別)其總結(jié)。
Hibernate提供了一系列數(shù)據(jù)記錄,其記錄的內(nèi)容包括從最基本的信息到與具體場(chǎng)景的特殊信息。所有的測(cè)量值都可以由 Statistics接口進(jìn)行訪問(wèn),主要分為三類(lèi):
-
使用Session的普通數(shù)據(jù)記錄,例如打開(kāi)的Session的個(gè)數(shù)、取得的JDBC的連接數(shù)等;
-
實(shí)體、集合、查詢、緩存等內(nèi)容的統(tǒng)一數(shù)據(jù)記錄
-
和具體實(shí)體、集合、查詢、緩存相關(guān)的詳細(xì)數(shù)據(jù)記錄
例如:你可以檢查緩存的命中成功次數(shù),緩存的命中失敗次數(shù),實(shí)體、集合和查詢的使用概率,查詢的平均時(shí)間等。請(qǐng)注意 Java中時(shí)間的近似精度是毫秒。Hibernate的數(shù)據(jù)精度和具體的JVM有關(guān),在有些平臺(tái)上其精度甚至只能精確到10秒。
你可以直接使用getter方法得到全局?jǐn)?shù)據(jù)記錄(例如,和具體的實(shí)體、集合、緩存區(qū)無(wú)關(guān)的數(shù)據(jù)),你也可以在具體查詢中通過(guò)標(biāo)記實(shí)體名、 或HQL、SQL語(yǔ)句得到某實(shí)體的數(shù)據(jù)記錄。請(qǐng)參考Statistics、EntityStatistics、 CollectionStatistics、SecondLevelCacheStatistics、 和QueryStatistics的API文檔以抓取更多信息。下面的代碼則是個(gè)簡(jiǎn)單的例子:
Statistics stats = HibernateUtil.sessionFactory.getStatistics(); double queryCacheHitCount = stats.getQueryCacheHitCount(); double queryCacheMissCount = stats.getQueryCacheMissCount(); double queryCacheHitRatio = queryCacheHitCount / (queryCacheHitCount + queryCacheMissCount); log.info("Query Hit ratio:" + queryCacheHitRatio); EntityStatistics entityStats = stats.getEntityStatistics( Cat.class.getName() ); long changes = entityStats.getInsertCount() + entityStats.getUpdateCount() + entityStats.getDeleteCount(); log.info(Cat.class.getName() + " changed " + changes + "times" );
如果你想得到所有實(shí)體、集合、查詢和緩存區(qū)的數(shù)據(jù),你可以通過(guò)以下方法獲得實(shí)體、集合、查詢和緩存區(qū)列表: getQueries()、getEntityNames()、 getCollectionRoleNames()和 getSecondLevelCacheRegionNames()。
Hibernate程序性能優(yōu)化的考慮要點(diǎn) |
MENGCHUCHEN |
|