抓取策略(fetching strategy) 是指:當應用程序需要在(Hibernate實體對象圖的)關聯關系間進行導航的時候, Hibernate如何獲取關聯對象的策略。抓取策略可以在O/R映射的元數據中聲明,也可以在特定的HQL 或條件查詢(Criteria Query)中重載聲明。
Hibernate3 定義了如下幾種抓取策略:
-
連接抓取(Join fetching) - Hibernate通過 在SELECT語句使用OUTER JOIN(外連接)來 獲得對象的關聯實例或者關聯集合。
-
查詢抓取(Select fetching) - 另外發送一條 SELECT 語句抓取當前對象的關聯實體或集合。除非你顯式的指定lazy="false"禁止 延遲抓取(lazy fetching),否則只有當你真正訪問關聯關系的時候,才會執行第二條select語句。
-
子查詢抓取(Subselect fetching) - 另外發送一條SELECT 語句抓取在前面查詢到(或者抓取到)的所有實體對象的關聯集合。除非你顯式的指定lazy="false" 禁止延遲抓取(lazy fetching),否則只有當你真正訪問關聯關系的時候,才會執行第二條select語句。
-
批量抓取(Batch fetching) - 對查詢抓取的優化方案, 通過指定一個主鍵或外鍵列表,Hibernate使用單條SELECT語句獲取一批對象實例或集合。
Hibernate會區分下列各種情況:
-
Immediate fetching,立即抓取 - 當宿主被加載時,關聯、集合或屬性被立即抓取。
-
Lazy collection fetching,延遲集合抓取- 直到應用程序對集合進行了一次操作時,集合才被抓取。(對集合而言這是默認行為。)
-
Proxy fetching,代理抓取 - 對返回單值的關聯而言,當其某個方法被調用,而非對其關鍵字進行get操作時才抓取。
-
Lazy attribute fetching,屬性延遲加載 - 對屬性或返回單值的關聯而言,當其實例變量被訪問的時候進行抓取(需要運行時字節碼強化)。這一方法很少是必要的。
這里有兩個正交的概念:關聯何時被抓取,以及被如何抓取(會采用什么樣的SQL語句)。不要混淆它們!我們使用抓取來改善性能。我們使用延遲來定義一些契約,對某特定類的某個脫管的實例,知道有哪些數據是可以使用的。
默認情況下,Hibernate 3對集合使用延遲select抓取,對返回單值的關聯使用延遲代理抓取。對幾乎是所有的應用而言,其絕大多數的關聯,這種策略都是有效的。
注意:假若你設置了hibernate.default_batch_fetch_size,Hibernate會對延遲加載采取批量抓取優化措施(這種優化也可能會在更細化的級別打開)。
然而,你必須了解延遲抓取帶來的一個問題。在一個打開的Hibernate session上下文之外調用延遲集合會導致一次意外。比如:
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關閉后,permessions集合將是未實例化的、不再可用,因此無法正常載入其狀態。 Hibernate對脫管對象不支持延遲實例化. 這里的修改方法是:將permissions讀取數據的代碼 移到tx.commit()之前。
除此之外,通過對關聯映射指定lazy="false",我們也可以使用非延遲的集合或關聯。但是, 對絕大部分集合來說,更推薦使用延遲方式抓取數據。如果在你的對象模型中定義了太多的非延遲關聯,Hibernate最終幾乎需要在每個事務中載入整個數據庫到內存中!
但是,另一方面,在一些特殊的事務中,我們也經常需要使用到連接抓取(它本身上就是非延遲的),以代替查詢抓取。 下面我們將會很快明白如何具體的定制Hibernate中的抓取策略。在Hibernate3中,具體選擇哪種抓取策略的機制是和選擇 單值關聯或集合關聯相一致的。
查詢抓取(默認的)在N+1查詢的情況下是極其脆弱的,因此我們可能會要求在映射文檔中定義使用連接抓取:
<set name="permissions" fetch="join"> <key column="userId"/> <one-to-many class="Permission"/> </set
<many-to-one name="mother" class="Cat" fetch="join"/>
在映射文檔中定義的抓取策略將會有產生以下影響:
-
通過get()或load()方法取得數據。
-
只有在關聯之間進行導航時,才會隱式的取得數據(延遲抓取)。
-
條件查詢
通常情況下,我們并不使用映射文檔進行抓取策略的定制。更多的是,保持其默認值,然后在特定的事務中, 使用HQL的左連接抓取(left join fetch) 對其進行重載。這將通知 Hibernate在第一次查詢中使用外部關聯(outer join),直接得到其關聯數據。 在條件查詢 API中,應該調用 setFetchMode(FetchMode.JOIN)語句。
也許你喜歡僅僅通過條件查詢,就可以改變get() 或 load()語句中的數據抓取策略。例如:
User user = (User) session.createCriteria(User.class) .setFetchMode("permissions", FetchMode.JOIN) .add( Restrictions.idEq(userId) ) .uniqueResult();
(這就是其他ORM解決方案的“抓取計劃(fetch plan)”在Hibernate中的等價物。)
截然不同的一種避免N+1次查詢的方法是,使用二級緩存。
在Hinerbate中,對集合的延遲抓取的采用了自己的實現方法。但是,對于單端關聯的延遲抓取,則需要采用 其他不同的機制。單端關聯的目標實體必須使用代理,Hihernate在運行期二進制級(通過優異的CGLIB庫), 為持久對象實現了延遲載入代理。
默認的,Hibernate3將會為所有的持久對象產生代理(在啟動階段),然后使用他們實現 多對一(many-to-one)關聯和一對一(one-to-one) 關聯的延遲抓取。
在映射文件中,可以通過設置proxy屬性為目標class聲明一個接口供代理接口使用。 默認的,Hibernate將會使用該類的一個子類。 注意:被代理的類必須實現一個至少包可見的默認構造函數,我們建議所有的持久類都應擁有這樣的構造函數
在如此方式定義一個多態類的時候,有許多值得注意的常見性的問題,例如:
<class name="Cat" proxy="Cat"> ...... <subclass name="DomesticCat"> ..... </subclass> </class>
首先,Cat實例永遠不可以被強制轉換為DomesticCat, 即使它本身就是DomesticCat實例。
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
雖然如此,但實際情況并沒有看上去那么糟糕。雖然我們現在有兩個不同的引用,分別指向這兩個不同的代理對象, 但實際上,其底層應該是同一個實例對象:
cat.setWeight(11.0); // hit the db to initialize the proxy System.out.println( dc.getWeight() ); // 11.0
第三,你不能對“final類”或“具有final方法的類”使用CGLIB代理。
最后,如果你的持久化對象在實例化時需要某些資源(例如,在實例化方法、默認構造方法中), 那么代理對象也同樣需要使用這些資源。實際上,代理類是持久化類的子類。
這些問題都源于Java的單根繼承模型的天生限制。如果你希望避免這些問題,那么你的每個持久化類必須實現一個接口, 在此接口中已經聲明了其業務方法。然后,你需要在映射文檔中再指定這些接口。例如:
<class name="CatImpl" proxy="Cat"> ...... <subclass name="DomesticCatImpl" proxy="DomesticCat"> ..... </subclass> </class>
這里CatImpl實現了Cat接口, DomesticCatImpl實現DomesticCat接口。 在load()、iterate()方法中就會返回 Cat和DomesticCat的代理對象。 (注意list()并不會返回代理對象。)
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();
這里,對象之間的關系也將被延遲載入。這就意味著,你應該將屬性聲明為Cat,而不是CatImpl。
但是,在有些方法中是不需要使用代理的。例如:
-
equals()方法,如果持久類沒有重載equals()方法。
-
hashCode()方法,如果持久類沒有重載hashCode()方法。
-
標志符的getter方法。
Hibernate將會識別出那些重載了equals()、或hashCode()方法的持久化類。
在Session范圍之外訪問未初始化的集合或代理,Hibernate將會拋出LazyInitializationException異常。 也就是說,在分離狀態下,訪問一個實體所擁有的集合,或者訪問其指向代理的屬性時,會引發此異常。
有時候我們需要保證某個代理或者集合在Session關閉前就已經被初始化了。 當然,我們可以通過強行調用cat.getSex()或者cat.getKittens().size()之類的方法來確保這一點。 但是這樣的程序會造成讀者的疑惑,也不符合通常的代碼規范。
靜態方法Hibernate.initialized() 為你的應用程序提供了一個便捷的途徑來延遲加載集合或代理。 只要它的Session處于open狀態,Hibernate.initialize(cat) 將會為cat強制對代理實例化。 同樣,Hibernate.initialize( cat.getKittens() ) 對kittens的集合具有同樣的功能。
還有另外一種選擇,就是保持Session一直處于open狀態,直到所有需要的集合或代理都被載入。 在某些應用架構中,特別是對于那些使用Hibernate進行數據訪問的代碼,以及那些在不同應用層和不同物理進程中使用Hibernate的代碼。 在集合實例化時,如何保證Session處于open狀態經常會是一個問題。有兩種方法可以解決此問題:
-
在一個基于Web的應用中,可以利用servlet過濾器(filter),在用戶請求(request)結束、頁面生成 結束時關閉Session(這里使用了在展示層保持打開Session模式(Open Session in View)), 當然,這將依賴于應用框架中異常需要被正確的處理。在返回界面給用戶之前,乃至在生成界面過程中發生異常的情況下, 正確關閉Session和結束事務將是非常重要的, Servlet過濾器必須如此訪問Session,才能保證正確使用Session。 我們推薦使用ThreadLocal 變量保存當前的Session (可以參考第 1.4 節 “與Cat同樂”的例子實現)。
-
在一個擁有單獨業務層的應用中,業務層必須在返回之前,為web層“準備”好其所需的數據集合。這就意味著 業務層應該載入所有表現層/web層所需的數據,并將這些已實例化完畢的數據返回。通常,應用程序應該 為web層所需的每個集合調用Hibernate.initialize()(這個調用必須發生咱session關閉之前); 或者使用帶有FETCH從句,或FetchMode.JOIN的Hibernate查詢, 事先取得所有的數據集合。如果你在應用中使用了Command模式,代替Session Facade , 那么這項任務將會變得簡單的多。
-
你也可以通過merge()或lock()方法,在訪問未實例化的集合(或代理)之前, 為先前載入的對象綁定一個新的Session。 顯然,Hibernate將不會,也不應該自動完成這些任務,因為這將引入一個特殊的事務語義。
有時候,你并不需要完全實例化整個大的集合,僅需要了解它的部分信息(例如其大小)、或者集合的部分內容。
你可以使用集合過濾器得到其集合的大小,而不必實例化整個集合:
( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()
這里的createFilter()方法也可以被用來有效的抓取集合的部分內容,而無需實例化整個集合:
s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();
Hibernate可以充分有效的使用批量抓取,也就是說,如果僅一個訪問代理(或集合),那么Hibernate將不載入其他未實例化的代理。 批量抓取是延遲查詢抓取的優化方案,你可以在兩種批量抓取方案之間進行選擇:在類級別和集合級別。
類/實體級別的批量抓取很容易理解。假設你在運行時將需要面對下面的問題:你在一個Session中載入了25個 Cat實例,每個Cat實例都擁有一個引用成員owner, 其指向Person,而Person類是代理,同時lazy="true"。 如果你必須遍歷整個cats集合,對每個元素調用getOwner()方法,Hibernate將會默認的執行25次SELECT查詢, 得到其owner的代理對象。這時,你可以通過在映射文件的Person屬性,顯式聲明batch-size,改變其行為:
<class name="Person" batch-size="10">...</class>
隨之,Hibernate將只需要執行三次查詢,分別為10、10、 5。
你也可以在集合級別定義批量抓取。例如,如果每個Person都擁有一個延遲載入的Cats集合, 現在,Sesssion中載入了10個person對象,遍歷person集合將會引起10次SELECT查詢, 每次查詢都會調用getCats()方法。如果你在Person的映射定義部分,允許對cats批量抓取, 那么,Hibernate將可以預先抓取整個集合。請看例子:
<class name="Person"> <set name="cats" batch-size="3"> ... </set> </class>
如果整個的batch-size是3(筆誤?),那么Hibernate將會分四次執行SELECT查詢, 按照3、3、3、1的大小分別載入數據。這里的每次載入的數據量還具體依賴于當前Session中未實例化集合的個數。
如果你的模型中有嵌套的樹狀結構,例如典型的帳單-原料結構(bill-of-materials pattern),集合的批量抓取是非常有用的。 (盡管在更多情況下對樹進行讀取時,嵌套集合(nested set)或原料路徑(materialized path)(××) 是更好的解決方法。)
假若一個延遲集合或單值代理需要抓取,Hibernate會使用一個subselect重新運行原來的查詢,一次性讀入所有的實例。這和批量抓取的實現方法是一樣的,不會有破碎的加載。
Hibernate3對單獨的屬性支持延遲抓取,這項優化技術也被稱為組抓取(fetch groups)。 請注意,該技術更多的屬于市場特性。在實際應用中,優化行讀取比優化列讀取更重要。但是,僅載入類的部分屬性在某些特定情況下會有用,例如在原有表中擁有幾百列數據、數據模型無法改動的情況下。
可以在映射文件中對特定的屬性設置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>
屬性的延遲載入要求在其代碼構建時加入二進制指示指令(bytecode instrumentation),如果你的持久類代碼中未含有這些指令, Hibernate將會忽略這些屬性的延遲設置,仍然將其直接載入。
你可以在Ant的Task中,進行如下定義,對持久類代碼加入“二進制指令。”
<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>
還有一種可以優化的方法,它使用HQL或條件查詢的投影(projection)特性,可以避免讀取非必要的列, 這一點至少對只讀事務是非常有用的。它無需在代碼構建時“二進制指令”處理,因此是一個更加值得選擇的解決方法。
有時你需要在HQL中通過抓取所有屬性,強行抓取所有內容。
Hibernate的Session在事務級別進行持久化數據的緩存操作。 當然,也有可能分別為每個類(或集合),配置集群、或JVM級別(SessionFactory級別)的緩存。 你甚至可以為之插入一個集群的緩存。注意,緩存永遠不知道其他應用程序對持久化倉庫(數據庫)可能進行的修改 (即使可以將緩存數據設定為定期失效)。
默認情況下,Hibernate使用EHCache進行JVM級別的緩存(目前,Hibernate已經廢棄了對JCS的支持,未來版本中將會去掉它)。 你可以通過設置hibernate.cache.provider_class屬性,指定其他的緩存策略, 該緩存策略必須實現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.) |
類或者集合映射的“<cache>元素”可以有下列形式:
<cache
usage="transactional|read-write|nonstrict-read-write|read-only" (1)
/>
(1) |
usage說明了緩存的策略: transactional、 read-write、 nonstrict-read-write或 read-only。 |
另外(首選?), 你可以在hibernate.cfg.xml中指定<class-cache>和 <collection-cache> 元素。
這里的usage 屬性指明了緩存并發策略(cache concurrency strategy)。
如果你的應用程序只需讀取一個持久化類的實例,而無需對其修改, 那么就可以對其進行只讀 緩存。這是最簡單,也是實用性最好的方法。甚至在集群中,它也能完美地運作。
<class name="eg.Immutable" mutable="false"> <cache usage="read-only"/> .... </class>
如果應用程序需要更新數據,那么使用讀/寫緩存 比較合適。 如果應用程序要求“序列化事務”的隔離級別(serializable transaction isolation level),那么就決不能使用這種緩存策略。 如果在JTA環境中使用緩存,你必須指定hibernate.transaction.manager_lookup_class屬性的值, 通過它,Hibernate才能知道該應用程序中JTA的TransactionManager的具體策略。 在其它環境中,你必須保證在Session.close()、或Session.disconnect()調用前, 整個事務已經結束。 如果你想在集群環境中使用此策略,你必須保證底層的緩存實現支持鎖定(locking)。Hibernate內置的緩存策略并不支持鎖定功能。
<class name="eg.Cat" .... > <cache usage="read-write"/> .... <set name="kittens" ... > <cache usage="read-write"/> .... </set> </class>
如果應用程序只偶爾需要更新數據(也就是說,兩個事務同時更新同一記錄的情況很不常見),也不需要十分嚴格的事務隔離, 那么比較適合使用非嚴格讀/寫緩存策略。如果在JTA環境中使用該策略, 你必須為其指定hibernate.transaction.manager_lookup_class屬性的值, 在其它環境中,你必須保證在Session.close()、或Session.disconnect()調用前, 整個事務已經結束。
Hibernate的事務緩存策略提供了全事務的緩存支持, 例如對JBoss TreeCache的支持。這樣的緩存只能用于JTA環境中,你必須指定 為其hibernate.transaction.manager_lookup_class屬性。
沒有一種緩存提供商能夠支持上列的所有緩存并發策略。下表中列出了各種提供器、及其各自適用的并發策略。
無論何時,當你給save()、update()或 saveOrUpdate()方法傳遞一個對象時,或使用load()、 get()、list()、iterate() 或scroll()方法獲得一個對象時, 該對象都將被加入到Session的內部緩存中。
當隨后flush()方法被調用時,對象的狀態會和數據庫取得同步。 如果你不希望此同步操作發生,或者你正處理大量對象、需要對有效管理內存時,你可以調用evict() 方法,從一級緩存中去掉這些對象及其集合。
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還提供了一個contains()方法,用來判斷某個實例是否處于當前session的緩存中。
如若要把所有的對象從session緩存中徹底清除,則需要調用Session.clear()。
對于二級緩存來說,在SessionFactory中定義了許多方法, 清除緩存中實例、整個類、集合實例或者整個集合。
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參數用于控制具體的Session如何與二級緩存進行交互。
-
CacheMode.NORMAL - 從二級緩存中讀、寫數據。
-
CacheMode.GET - 從二級緩存中讀取數據,僅在數據更新時對二級緩存寫數據。
-
CacheMode.PUT - 僅向二級緩存寫數據,但不從二級緩存中讀數據。
-
CacheMode.REFRESH - 僅向二級緩存寫數據,但不從二級緩存中讀數據。通過 hibernate.cache.use_minimal_puts的設置,強制二級緩存從數據庫中讀取數據,刷新緩存內容。
如若需要查看二級緩存或查詢緩存區域的內容,你可以使用統計(Statistics) API。
Map cacheEntries = sessionFactory.getStatistics() .getSecondLevelCacheStatistics(regionName) .getEntries();
此時,你必須手工打開統計選項。可選的,你可以讓Hibernate更人工可讀的方式維護緩存內容。
hibernate.generate_statistics true hibernate.cache.use_structured_entries true
查詢的結果集也可以被緩存。只有當經常使用同樣的參數進行查詢時,這才會有些用處。 要使用查詢緩存,首先你必須打開它:
hibernate.cache.use_query_cache true
該設置將會創建兩個緩存區域 - 一個用于保存查詢結果集(org.hibernate.cache.StandardQueryCache); 另一個則用于保存最近查詢的一系列表的時間戳(org.hibernate.cache.UpdateTimestampsCache)。 請注意:在查詢緩存中,它并不緩存結果集中所包含的實體的確切狀態;它只緩存這些實體的標識符屬性的值、以及各值類型的結果。 所以查詢緩存通常會和二級緩存一起使用。
絕大多數的查詢并不能從查詢緩存中受益,所以Hibernate默認是不進行查詢緩存的。如若需要進行緩存,請調用 Query.setCacheable(true)方法。這個調用會讓查詢在執行過程中時先從緩存中查找結果, 并將自己的結果集放到緩存中去。
如果你要對查詢緩存的失效政策進行精確的控制,你必須調用Query.setCacheRegion()方法, 為每個查詢指定其命名的緩存區域。
List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger") .setEntity("blogger", blogger) .setMaxResults(15) .setCacheable(true) .setCacheRegion("frontpages") .list();
如果查詢需要強行刷新其查詢緩存區域,那么你應該調用Query.setCacheMode(CacheMode.REFRESH)方法。 這對在其他進程中修改底層數據(例如,不通過Hibernate修改數據),或對那些需要選擇性更新特定查詢結果集的情況特別有用。 這是對SessionFactory.evictQueries()的更為有效的替代方案,同樣可以清除查詢緩存區域。
前面我們已經對集合進行了足夠的討論。本段中,我們將著重講述集合在運行時的事宜。
Hibernate定義了三種基本類型的集合:
-
值數據集合
-
一對多關聯
-
多對多關聯
這個分類是區分了不同的表和外鍵關系類型,但是它沒有告訴我們關系模型的所有內容。 要完全理解他們的關系結構和性能特點,我們必須同時考慮“用于Hibernate更新或刪除集合行數據的主鍵的結構”。 因此得到了如下的分類:
-
有序集合類
-
集合(sets)
-
包(bags)
所有的有序集合類(maps, lists, arrays)都擁有一個由<key>和 <index>組成的主鍵。 這種情況下集合類的更新是非常高效的——主鍵已經被有效的索引,因此當Hibernate試圖更新或刪除一行時,可以迅速找到該行數據。
集合(sets)的主鍵由<key>和其他元素字段構成。 對于有些元素類型來說,這很低效,特別是組合元素或者大文本、大二進制字段; 數據庫可能無法有效的對復雜的主鍵進行索引。 另一方面,對于一對多、多對多關聯,特別是合成的標識符來說,集合也可以達到同樣的高效性能。( 附注:如果你希望SchemaExport為你的<set>創建主鍵, 你必須把所有的字段都聲明為not-null="true"。)
<idbag>映射定義了代理鍵,因此它總是可以很高效的被更新。事實上, <idbag>擁有著最好的性能表現。
Bag是最差的。因為bag允許重復的元素值,也沒有索引字段,因此不可能定義主鍵。 Hibernate無法判斷出重復的行。當這種集合被更改時,Hibernate將會先完整地移除 (通過一個(in a single DELETE))整個集合,然后再重新創建整個集合。 因此Bag是非常低效的。
請注意:對于一對多關聯來說,“主鍵”很可能并不是數據庫表的物理主鍵。 但就算在此情況下,上面的分類仍然是有用的。(它仍然反映了Hibernate在集合的各數據行中是如何進行“定位”的。)
根據我們上面的討論,顯然有序集合類型和大多數set都可以在增加、刪除、修改元素中擁有最好的性能。
可論證的是對于多對多關聯、值數據集合而言,有序集合類比集合(set)有一個好處。因為Set的內在結構, 如果“改變”了一個元素,Hibernate并不會更新(UPDATE)這一行。 對于Set來說,只有在插入(INSERT)和刪除(DELETE) 操作時“改變”才有效。再次強調:這段討論對“一對多關聯”并不適用。
注意到數組無法延遲載入,我們可以得出結論,list, map和idbags是最高效的(非反向)集合類型,set則緊隨其后。 在Hibernate中,set應該時最通用的集合類型,這時因為“set”的語義在關系模型中是最自然的。
但是,在設計良好的Hibernate領域模型中,我們通常可以看到更多的集合事實上是帶有inverse="true" 的一對多的關聯。對于這些關聯,更新操作將會在多對一的這一端進行處理。因此對于此類情況,無需考慮其集合的更新性能。
在把bag扔進水溝之前,你必須了解,在一種情況下,bag的性能(包括list)要比set高得多: 對于指明了inverse="true"的集合類(比如說,標準的雙向的一對多關聯), 我們可以在未初始化(fetch)包元素的情況下直接向bag或list添加新元素! 這是因為Collection.add())或者Collection.addAll() 方法 對bag或者List總是返回true(這點與與Set不同)。因此對于下面的相同代碼來說,速度會快得多。
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();
偶爾的,逐個刪除集合類中的元素是相當低效的。Hibernate并沒那么笨, 如果你想要把整個集合都刪除(比如說調用list.clear()),Hibernate只需要一個DELETE就搞定了。
假設我們在一個長度為20的集合類中新增加了一個元素,然后再刪除兩個。 Hibernate會安排一條INSERT語句和兩條DELETE語句(除非集合類是一個bag)。 這當然是顯而易見的。
但是,假設我們刪除了18個數據,只剩下2個,然后新增3個。則有兩種處理方式:
-
逐一的刪除這18個數據,再新增三個;
-
刪除整個集合類(只用一句DELETE語句),然后增加5個數據。
Hibernate還沒那么聰明,知道第二種選擇可能會比較快。 (也許讓Hibernate不這么聰明也是好事,否則可能會引發意外的“數據庫觸發器”之類的問題。)
幸運的是,你可以強制使用第二種策略。你需要取消原來的整個集合類(解除其引用), 然后再返回一個新的實例化的集合類,只包含需要的元素。有些時候這是非常有用的。
顯然,一次性刪除并不適用于被映射為inverse="true"的集合。
沒有監測和性能參數而進行優化是毫無意義的。Hibernate為其內部操作提供了一系列的示意圖,因此可以從 每個SessionFactory抓取其統計數據。
你可以有兩種方式訪問SessionFactory的數據記錄,第一種就是自己直接調用 sessionFactory.getStatistics()方法讀取、顯示統計數據。
此外,如果你打開StatisticsService MBean選項,那么Hibernate則可以使用JMX技術 發布其數據記錄。你可以讓應用中所有的SessionFactory同時共享一個MBean,也可以每個 SessionFactory分配一個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:仍需要說明的是:在第一個例子中,我們直接得到和使用MBean;而在第二個例子中,在使用MBean之前 我們則需要給出SessionFactory的JNDI名,使用hibernateStatsBean.setSessionFactoryJNDIName("my/JNDI/Name") 得到SessionFactory,然后將MBean保存于其中。
你可以通過以下方法打開或關閉SessionFactory的監測功能:
-
在配置期間,將hibernate.generate_statistics設置為true或false;
-
在運行期間,則可以可以通過sf.getStatistics().setStatisticsEnabled(true) 或hibernateStatsBean.setStatisticsEnabled(true)
你也可以在程序中調用clear()方法重置統計數據,調用logSummary() 在日志中記錄(info級別)其總結。
Hibernate提供了一系列數據記錄,其記錄的內容包括從最基本的信息到與具體場景的特殊信息。所有的測量值都可以由 Statistics接口進行訪問,主要分為三類:
-
使用Session的普通數據記錄,例如打開的Session的個數、取得的JDBC的連接數等;
-
實體、集合、查詢、緩存等內容的統一數據記錄
-
和具體實體、集合、查詢、緩存相關的詳細數據記錄
例如:你可以檢查緩存的命中成功次數,緩存的命中失敗次數,實體、集合和查詢的使用概率,查詢的平均時間等。請注意 Java中時間的近似精度是毫秒。Hibernate的數據精度和具體的JVM有關,在有些平臺上其精度甚至只能精確到10秒。
你可以直接使用getter方法得到全局數據記錄(例如,和具體的實體、集合、緩存區無關的數據),你也可以在具體查詢中通過標記實體名、 或HQL、SQL語句得到某實體的數據記錄。請參考Statistics、EntityStatistics、 CollectionStatistics、SecondLevelCacheStatistics、 和QueryStatistics的API文檔以抓取更多信息。下面的代碼則是個簡單的例子:
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" );
如果你想得到所有實體、集合、查詢和緩存區的數據,你可以通過以下方法獲得實體、集合、查詢和緩存區列表: getQueries()、getEntityNames()、 getCollectionRoleNames()和 getSecondLevelCacheRegionNames()。
Hibernate程序性能優化的考慮要點 |
MENGCHUCHEN |
|