4.6 HQL調(diào)優(yōu)
4.6.1 索引調(diào)優(yōu)
HQL看起來和SQL很相似。從HQL的WHERE子句中通常可以猜到相應(yīng)的SQL WHERE子句。WHERE子句中的字段決定了數(shù)據(jù)庫將選擇的索引。
大多數(shù)Hibernate開發(fā)者所常犯的一個錯誤是無論何時,當(dāng)需要新WHERE子句的時候都會創(chuàng)建一個新的索引。因為索引會帶來額外的數(shù)據(jù)更新開銷,所以應(yīng)該爭取創(chuàng)建少量索引來覆蓋盡可能多的查詢。
4.1節(jié)讓你使用一個集合來處理所有可能的數(shù)據(jù)搜索條件。如果這不太實際,那么你可以使用后端剖析工具來創(chuàng)建一個針對應(yīng)用程序涉及的所有SQL的集合。基于那些搜索條件的分類,你最終會得到一個小的索引集。與此同時,還可以嘗試向WHERE子句中添加額外的謂語來匹配其他WHERE子句。
范例7
有兩個UI搜索器和一個后端守護(hù)進(jìn)程搜索器來搜索名為iso_deals的表。第一個UI搜索器在unexpectedFlag、dealStatus、tradeDate和isold屬性上有謂語。
第二個UI搜索器基于用戶鍵入的過濾器,其中包括的內(nèi)容除tradeDate和isold以外還有其他屬性。開始時所有這些過濾器屬性都是可選的。
后端搜索器基于isold、participantCode和transactionType屬性。
經(jīng)過進(jìn)一步業(yè)務(wù)分析,發(fā)現(xiàn)第二個UI搜索器實際是基于一些隱式的unexpectedFlag和dealStatus值來選擇數(shù)據(jù)的。我們還讓tradeDate成為過濾器的必要屬性(為了使用數(shù)據(jù)庫索引,每個搜索過濾器都應(yīng)該有必要屬性)。
鑒于這一點,我們依次使用unexpectedFlag、dealStatus、tradeDate和isold構(gòu)造了一個復(fù)合索引。兩個UI搜索器都能共用它。(順序很重要,如果你的謂語以不同的順序指定這些屬性或在它們前羅列了其他屬性,數(shù)據(jù)庫就不會選擇該復(fù)合索引。)
后端搜索器和UI搜索器區(qū)別太大,因此我們不得不為它構(gòu)造另一個復(fù)合索引,依次使用isold、participantCode和transactionType。
4.6.2綁定參數(shù) vs.字符串拼接
既可以使用綁定參數(shù)構(gòu)造HQL的WHERE子句,也可以使用字符串拼接的方法,該決定對性能會有一定影響。使用綁定參數(shù)的原因是讓數(shù)據(jù)庫一次解析SQL,對后續(xù)的重復(fù)請求復(fù)用生成好的執(zhí)行計劃,這樣做節(jié)省了CPU時間和內(nèi)存。然而,為達(dá)到最優(yōu)的數(shù)據(jù)訪問效率,不同的綁定值可能需要不同的SQL執(zhí)行計劃。
例如,一小段數(shù)據(jù)范圍可能只返回數(shù)據(jù)總量的5%,而一大段數(shù)據(jù)范圍可能返回數(shù)據(jù)總量的90%。前者使用索引更好,而后者則最好使用全表掃描。
建議OLTP使用綁定參數(shù),數(shù)據(jù)倉庫使用字符串拼接,因為OLTP通常在一個事務(wù)中重復(fù)插入和更新數(shù)據(jù),只取少量數(shù)據(jù);數(shù)據(jù)倉庫通常只有少量SQL查詢,有一個確定的執(zhí)行計劃比節(jié)省CPU時間和內(nèi)存更為重要。
要是你知道你的OLTP搜索對不同綁定值應(yīng)該使用相同執(zhí)行計劃又該怎么辦呢?
Oracle 9i及以后版本在第一次調(diào)用綁定參數(shù)并生成執(zhí)行計劃時能探出參數(shù)值。后續(xù)調(diào)用不會再探測,而是重用之前的執(zhí)行計劃。
4.6.3聚合及排序
你可以在數(shù)據(jù)庫中進(jìn)行聚合和“order by”,也可以在應(yīng)用程序的服務(wù)層中事先加載所有數(shù)據(jù)然后做聚合和“order by”操作。推薦使用前者,因為數(shù)據(jù)庫在這方面通常會比你的應(yīng)用程序做得好。此外,這樣做還能節(jié)省網(wǎng)絡(luò)帶寬,這也是一種擁有跨數(shù)據(jù)庫移植性的做法。
當(dāng)你的應(yīng)用程序?qū)?shù)據(jù)聚合和排序有HQL不支持的特定業(yè)務(wù)規(guī)則時除外。
4.6.4覆蓋抓取策略
詳見4.7.1節(jié)。
4.6.5本地查詢
本地查詢調(diào)優(yōu)其實并不直接與HQL有關(guān)。但HQL的確可以讓你直接向底層數(shù)據(jù)庫傳遞本地查詢。我們并不建議這么做,因為本地查詢在數(shù)據(jù)庫間不可移植。
4.7抓取策略調(diào)優(yōu)
抓取策略決定了在應(yīng)用程序需要訪問關(guān)聯(lián)對象時,Hibernate以何種方式以及何時獲取關(guān)聯(lián)對象。HRD中的第20章“改善性能”對該主題作了很好的闡述,我們在此將關(guān)注它的使用方法。
4.7.1覆蓋抓取策略
不同的用戶可能會有不同的數(shù)據(jù)抓取要求。Hibernate允許在兩個地方定義數(shù)據(jù)抓取策略,一處是在映射元數(shù)據(jù)中,另一處是在HQL或Criteria中覆蓋它。
常見的做法是基于主要的抓取用例在映射元數(shù)據(jù)中定義默認(rèn)抓取策略,針對少數(shù)用例在HQL和Criteria中覆蓋抓取策略。
假設(shè)pojoA和pojoB是父子關(guān)系實例。如果根據(jù)業(yè)務(wù)規(guī)則,只是偶爾需要從實體兩端加載數(shù)據(jù),那你可以聲明一個延遲加載集合或代理抓取(proxy fetching)。當(dāng)你需要從實體兩端獲取數(shù)據(jù)時,可以用立即抓取(eager fetching)覆蓋默認(rèn)策略,例如使用HQL或Criteria配置連接抓取(join fetching)。
另一方面,如果業(yè)務(wù)規(guī)則在大多數(shù)時候需要從實體兩端加載數(shù)據(jù),那么你可以聲明立即抓取并在Criteria中設(shè)置延遲加載集合或代理抓取來覆蓋它(HQL目前還不支持這樣的覆蓋)。
4.7.2 N+1模式或是反模式?
select抓取會導(dǎo)致N+1問題。如果你知道自己總是需要從關(guān)聯(lián)中加載數(shù)據(jù),那么就該始終使用連接抓取。在下面兩個場景中,你可能會把N+1視為一種模式而非反模式。
第一種場景,你不知道用戶是否會訪問關(guān)聯(lián)對象。如果他/她沒有訪問,那么你贏了;否則你仍然需要額外的N次select SQL語句。這是一種令人左右為難的局面。
第二種場景,pojoA和很多其他POJO有one-to-many關(guān)聯(lián),例如pojoB和pojoC。使用立即的內(nèi)連接或外連接抓取會在結(jié)果集中將pojoA重復(fù)很多次。當(dāng)pojoA中有很多非空屬性時,你不得不將大量數(shù)據(jù)加載到持久層中。這種加載需要很多時間,既有網(wǎng)絡(luò)帶寬的原因,如果Hibernate的會話是有狀態(tài)的,其中也會有會話緩存的原因(內(nèi)存消耗和GC暫停)。
如果你有一個很長的one-to-many關(guān)聯(lián)鏈,例如從pojoA到pojoB到pojoC以此類推,情況也是類似的。
你也許會去使用HQL中的DISTINCT關(guān)鍵字或Cirteria中的distinct功能或是Java的Set接口來消除重復(fù)數(shù)據(jù)。但所有這些都是在Hibernate(在持久層)中實現(xiàn)的,而非數(shù)據(jù)庫中。
如果基于你的網(wǎng)絡(luò)和內(nèi)存配置的測試表明N+1性能更好,那么你可以使用批量抓取、subselect抓取或二級緩存來做進(jìn)一步調(diào)優(yōu)。
范例8
以下是一個使用批量抓取的HBM文件片段:
<class name="pojoA" table="pojoA">
…
<set name="pojoBs" fetch="select" batch-size="10">
<key column="pojoa_id"/>
…
</set>
</class>
以下是多端pojoB生成的SQL:
select … from pojoB where pojoa_id in(?,?,?,?,?, ?,?,?,?,?);
問號數(shù)量與batch-size值相等。因此N次額外的關(guān)于pojoB的select SQL語句被減少到了N/10次。
如果將fetch="select"替換成fetch="subselect",pojoB生成的SQL語句就是這樣的:
select … from pojoB where pojoa_id in(select id from pojoA where …);
盡管N次額外的select減少到1次,但這只在重復(fù)運行pojoA的查詢開銷很低時才有好處。
如果pojoA中的pojoB集合很穩(wěn)定,或pojoB有pojoA的many-to-one關(guān)聯(lián),而且pojoA是只讀引用數(shù)據(jù),那么你可以使用二級緩存來緩存pojoA以消除N+1問題(4.8.1節(jié)中有一個例子)。
4.7.3延遲屬性抓取
除非有一張擁有很多你不需要的字段的遺留表,否則不應(yīng)該使用這種抓取策略,因為它的延遲屬性分組會帶來額外的SQL。
在業(yè)務(wù)分析和設(shè)計過程中,你應(yīng)該將不同數(shù)據(jù)獲取或修改分組放到不同的領(lǐng)域?qū)ο髮嶓w中,而不是使用這種抓取策略。
如果不能重新設(shè)計遺留表,可以使用HQL或Criteria提供的投影功能來獲取數(shù)據(jù)。
4.8 二級緩存調(diào)優(yōu)
HRD第20.2節(jié) “二級緩存”中的描述對大多數(shù)開發(fā)者來說過于簡單,無法做出選擇。3.3版及以后版本不再推薦使用基于“CacheProvider”的緩存,而用基于“RegionFactory”的緩存,這也讓人更糊涂了。但是就算是最新的3.5參考文檔也沒有提及如何使用新緩存方法。
出于下述考慮,我們將繼續(xù)關(guān)注于老方法:
4.8.1 基于CacheProvider的緩存機制
理解該機制是做出合理選擇的關(guān)鍵。關(guān)鍵的類/接口是CacheConcurrencyStrategy和它針對4中不同緩存使用的實現(xiàn)類,還有EntityUpdate/Delete/InsertAction。
針對并發(fā)緩存訪問,有三種實現(xiàn)模式:
- 針對“read-only”的只讀模式。
無論是鎖還是事務(wù)都沒影響,因為緩存自數(shù)據(jù)從數(shù)據(jù)庫加載后就不會改變。
- 針對“read-write”和“nonstrict-read-write”的非事務(wù)感知(non-transaction-aware)讀寫模式。
對緩存的更新發(fā)生在數(shù)據(jù)庫事務(wù)完成后。緩存需要支持鎖。
- 針對“transactional”的事務(wù)感知讀寫。
對緩存和數(shù)據(jù)庫的更新被包裝在同一個JTA事務(wù)中,這樣緩存與數(shù)據(jù)庫總是保持同步的。數(shù)據(jù)庫和緩存都必須支持JTA。盡管緩存事務(wù)內(nèi)部依賴于緩存鎖,但Hibernate不會顯式調(diào)用任何的緩存鎖函數(shù)。
以數(shù)據(jù)庫更新為例。EntityUpdateAction對于事務(wù)感知讀寫、“read-write”的非事務(wù)感知讀寫,還有“nonstrict-read-write”的非事務(wù)感知讀寫相應(yīng)有如下調(diào)用序列:
- 在一個JTA事務(wù)中更新數(shù)據(jù)庫;在同一個事務(wù)中更新緩存。
- 軟鎖緩存;在一個事務(wù)中更新數(shù)據(jù)庫;在上一個事務(wù)成功完成后更新緩存;否則釋放軟鎖。
軟鎖只是一種特定的緩存值失效表述方式,在它獲得新數(shù)據(jù)庫值前阻止其他事務(wù)讀寫緩存。那些事務(wù)會轉(zhuǎn)而直接讀取數(shù)據(jù)庫。
緩存必須支持鎖;事務(wù)支持則不是必須的。如果緩存是一個集群,“更新緩存”的調(diào)用會將新值推送給所有副本,這通常被稱為“推(push)”更新策略。
- 在一個事務(wù)中更新數(shù)據(jù)庫;在上一個事務(wù)完成前就清除緩存;為了安全起見,無論事務(wù)成功與否,在事務(wù)完成后再次清除緩存。
既不需要支持緩存鎖,也不需要支持事務(wù)。如果是緩存集群,“清除緩存”調(diào)用會讓所有副本都失效,這通常被稱為“拉(pull)”更新策略。
對于實體的刪除或插入動作,或者集合變更,調(diào)用序列都是相似的。
實際上,最后兩個異步調(diào)用序列仍能保證數(shù)據(jù)庫和緩存的一致性(基本就是“read committed”的隔離了級別),這要歸功于第二個序列中的軟鎖和“更新數(shù)據(jù)庫”后的“更新緩存”,還有最后一個調(diào)用序列中的悲觀“清除緩存”。
基于上述分析,我們的建議是:
范例9
以下是一個ISO收費類型的HBM文件片段:
<class name="IsoChargeType">
<property name="isoId" column="ISO_ID" not-null="true"/>
<many-to-one name="estimateMethod" fetch="join" lazy="false"/>
<many-to-one name="allocationMethod" fetch="join" lazy="false"/>
<many-to-one name="chargeTypeCategory" fetch="join" lazy="false"/>
</class>
一些用戶只需要ISO收費類型本身;一些用戶既需要ISO收費類型,還需要它的三個關(guān)聯(lián)對象。簡單起見,開發(fā)者會立即加載所有三個關(guān)聯(lián)對象。如果項目中沒人負(fù)責(zé)Hibernate調(diào)優(yōu),這是很常見的。
4.7.1節(jié)中講過了最好的方法。因為所有的關(guān)聯(lián)對象都是只讀引用數(shù)據(jù),另一種方法是使用延遲抓取,打開這些對象的二級緩存以避免N+1問題。實際上前一種方法也能從引用數(shù)據(jù)緩存中獲益。
因為大多數(shù)項目都有很多被其他數(shù)據(jù)引用的只讀引用數(shù)據(jù),上述兩種方法都能改善全局系統(tǒng)性能。
4.8.2 RegionFactory
下表是新老兩種方法中對應(yīng)的主要類/接口:
新方法
|
老方法
|
RegionFactory
|
CacheProvider
|
Region
|
Cache
|
EntityRegionAccessStrategy
|
CacheConcurrencyStrategy
|
CollectionRegionAccessStrategy
|
CacheConcurrencyStrategy
|
第一個改進(jìn)是RegionFactory構(gòu)建了特定的Region,例如EntityRegion和TransactionRegion,而不是使用一個通用的訪問Region。第二個改進(jìn)是對于特定緩存的“usage”屬性值,Region要求構(gòu)建自己的訪問策略,而不是所有Region都一直使用CacheConcurrencyStrategy的4種實現(xiàn)。
要使用新方法,應(yīng)該設(shè)置factory_class而非provider_class配置屬性。以Ehcache 2.0為例:
<property name="hibernate.cache.region.factory_class">
net.sf.ehcache.hibernate.EhCacheRegionFactory
</property>
其他相關(guān)的Hibernate緩存配置都和老方法一樣。
新方法也能向后兼容遺留方法。如果還是只配了CacheProvider,新方法中將使用下列自說明(self-explanatory)適配器和橋隱式地調(diào)用老的接口/類:
RegionFactoryCacheProviderBridge、EntityRegionAdapter、CollectionRegionAdapter、QueryResultsRegionAdapter、EntityAccessStrategyAdapter和CollectionAccessStrategyAdapter
4.8.3 查詢緩存
二級緩存也能緩存查詢結(jié)果。如果查詢開銷很大而且要重復(fù)運行,這也會很有幫助。
4.9批量處理調(diào)優(yōu)
大多數(shù)Hibernate的功能都很適合那些每個事務(wù)都通常只處理少量數(shù)據(jù)的OLTP系統(tǒng)。但是,如果你有一個數(shù)據(jù)倉庫或者事務(wù)需要處理大量數(shù)據(jù),那么就另當(dāng)別論了。
4.9.1使用有狀態(tài)會話的非DML風(fēng)格批處理
如果你已經(jīng)在使用常規(guī)會話了,那這是最自然的方法。你需要做三件事:
- 配置下列3個屬性以開啟批處理特性:
hibernate.jdbc.batch_size 30
hibernate.jdbc.batch_versioned_data true
hibernate.cache.use_second_level_cache false
batch_size設(shè)置為正值會開啟JDBC2的批量更新,Hibernate的建議值是5到30。基于我們的測試,極低值和極高值性能都很差。只要取值在合理范圍內(nèi),區(qū)別就只有幾秒而已。如果網(wǎng)絡(luò)夠快,這個結(jié)果是一定的。
第二個配置設(shè)為true,這要求JDBC驅(qū)動在executeBatch()方法中返回正確的行數(shù)。對于Oracle用戶而言,批量更新時不能將其設(shè)為true。請閱讀Oracle的《JDBC Developer’s Guide and Reference》中的“標(biāo)準(zhǔn)批處理的Oracle實現(xiàn)中的更新計數(shù)”(Update Counts in the Oracle Implementation of Standard Batching)以獲得更多詳細(xì)信息。因為它對批量插入來說還是安全的,所以你可以為批量插入創(chuàng)建單獨的專用數(shù)據(jù)源。最后一個配置項是可選的,因為你可以在會話中顯式關(guān)閉二級緩存。
- 像如下范例中那樣定期刷新(flush)并清除一級會話緩存:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
for ( int i=0; i<100000; i++ ) {
Customer customer = new Customer(.....);
//if your hibernate.cache.use_second_level_cache is true, call the following:
session.setCacheMode(CacheMode.IGNORE);
session.save(customer);
if (i % 50 == 0) { //50, same as the JDBC batch size
//flush a batch of inserts and release memory:
session.flush();
session.clear();
}
}
tx.commit();
session.close();
批處理通常不需要數(shù)據(jù)緩存,否則你會將內(nèi)存耗盡并大量增加GC開銷。如果內(nèi)存有限,那這種情況會很明顯。
- 總是將批量插入嵌套在事務(wù)中。
每次事務(wù)修改的對象數(shù)量越少就意味著會有更多數(shù)據(jù)庫提交,正如4.5節(jié)所述每次提交都會帶來磁盤相關(guān)的開銷。
另一方面,每次事務(wù)修改的對象數(shù)量越多就意味著鎖定變更時間越長,同時數(shù)據(jù)庫需要更大的redo log。
4.9.2使用無狀態(tài)會話的非DML風(fēng)格批處理
無狀態(tài)會話執(zhí)行起來比上一種方法更好,因為它只是JDBC的簡單包裝,而且可以繞開很多常規(guī)會話要求的操作。例如,它不需要會話緩存,也不和任何二級緩存或查詢緩存有交互。
然而它的用法并不簡單。尤其是它的操作并不會級聯(lián)到所關(guān)聯(lián)的實例上;你必須自己來處理它們。
4.9.3 DML風(fēng)格
使用DML風(fēng)格的插入、更新或刪除,你直接在數(shù)據(jù)庫中操作數(shù)據(jù),這和前兩種方法在Hibernate中操作數(shù)據(jù)的情況有所不同。
因為一個DML風(fēng)格的更新或刪除相當(dāng)于前兩種方法中的多個單獨的更新或刪除,所以如果更新或刪除中的WHERE子句暗示了恰當(dāng)?shù)臄?shù)據(jù)庫索引,那么使用DML風(fēng)格的操作能節(jié)省網(wǎng)絡(luò)開銷,執(zhí)行得更好。
強烈建議結(jié)合使用DML風(fēng)格操作和無狀態(tài)會話。如果使用有狀態(tài)會話,不要忘記在執(zhí)行DML前清除緩存,否則Hibernate將會更新或清除相關(guān)緩存(見下面的范例10)。
4.9.4批量加載
如果你的HQL或Criteria會返回很多數(shù)據(jù),那么要注意兩件事:
范例10
我們有一個后臺任務(wù),分段加載大量的IsoDeal數(shù)據(jù)用于后續(xù)處理。我們還會在分段數(shù)據(jù)交給下游系統(tǒng)處理前將其更新為處理中狀態(tài)。最大的一段有50萬行數(shù)據(jù)。以下是原始代碼中截取出來的一段:
Query query = session.createQuery("FROM IsoDeal d WHERE chunk-clause");
query.setLockMode("d", LockMode.UPGRADE); //for Inprocess status update
List<IsoDeal> isoDeals = query.list();
for (IsoDeal isoDeal : isoDeals) { //update status to Inprocess
isoDeal.setStatus("Inprocess");
}
return isoDeals;
包含上述代碼的方法加上了Spring 2.5聲明式事務(wù)的注解。加載并更新50萬行數(shù)據(jù)大約花了10分鐘。我們識別出了以下這些問題:
- 由于會話緩存和二級緩存的原因,系統(tǒng)會頻繁地內(nèi)存溢出。
- 就算沒有內(nèi)存溢出,當(dāng)內(nèi)存消耗很高時GC的開銷也會很大。
- 我們還未設(shè)置fetch_size。
- 就算我們設(shè)置了batch_size,for循環(huán)也創(chuàng)建了太多update SQL語句。
不幸的是Spring 2.5不支持Hibernate無狀態(tài)會話,所以我們只能關(guān)閉二級緩存;設(shè)置fetch_size;用DML風(fēng)格的更新來代替for循環(huán),以此改善性能。
但是,執(zhí)行時間還是要6分鐘。將Hibernate的日志級別調(diào)成trace后,我們發(fā)現(xiàn)是更新會話緩存造成了延時。通過在DML更新前清除會話緩存,我們將時間縮短到了4分鐘,全部都是將數(shù)據(jù)加載到會話緩存中花費的時間。
4.10 SQL生成調(diào)優(yōu)
本節(jié)將向你展示如何減少SQL生成的數(shù)量。
4.10.1 N+1抓取問題
“select抓取”策略會導(dǎo)致N+1問題。如果“連接抓取”策略適合你的話,你應(yīng)該始終使用該策略避免N+1問題。
但是,如果“連接抓取”策略執(zhí)行效果不理想,就像4.7.2節(jié)中那樣,你可以使用“subselect抓取”、“批量抓取”或“延遲集合抓取”來減少所需的額外SQL語句數(shù)。
4.10.2 Insert+Update問題
范例11
我們的ElectricityDeal與DealCharge有單向one-to-many關(guān)聯(lián),如下列HBM文件片段所示:
<class name="ElectricityDeal"
select-before-update="true" dynamic-update="true"
dynamic-insert="true">
<id name="key" column="ID">
<generator class="sequence">
<param name="sequence">SEQ_ELECTRICITY_DEALS</param>
</generator>
</id>
…
<set name="dealCharges" cascade="all-delete-orphan">
<key column="DEAL_KEY" not-null="false" update="true"
on-delete="noaction"/>
<one-to-many class="DealCharge"/>
</set> </class>
在“key”元素中,“not-null”和“update”對應(yīng)的默認(rèn)值是false和true,上述代碼為了明確這些取值,將它們寫了出來。
如果你想創(chuàng)建一個ElectricityDeal和十個DealCharge,會生成如下SQL語句:
- 1句ElectricityDeal的插入語句;
- 10句DealCharge的插入語句,其中不包括外鍵“DEAL_KEY”;
- 10句DealCharge字段“DEAL_KEY”的更新語句。
為了消除那額外的10句更新語句,可以在那10句DealCharge插入語句中包含“DEAL_KEY”,你需要將“not-null”和“update”分別修改為true和false。
另一種做法是使用雙向或many-to-one關(guān)聯(lián),讓DealCharge來管理關(guān)聯(lián)。
4.10.3 更新前執(zhí)行select
在范例11中,我們?yōu)镋lectricityDeal加上了select-before-update,這會對瞬時(transient)對象或分離(detached)對象產(chǎn)生額外的select語句,但卻能避免不必要的數(shù)據(jù)庫更新。
你應(yīng)該做出一些權(quán)衡,如果對象沒多少屬性,不需要防止不必要的數(shù)據(jù)庫更新,那么就不要使用該特性,因為你那些有限的數(shù)據(jù)既沒有太多網(wǎng)絡(luò)傳輸開銷,也不會帶來太多數(shù)據(jù)庫更新開銷。
如果對象的屬性較多,例如是一張大的遺留表,那你應(yīng)該開啟該特性,和“dynamic-update”結(jié)合使用以避免太多數(shù)據(jù)庫更新開銷。
4.10.4 級聯(lián)刪除
在范例11中,如果你想刪除1個ElectricityDeal和它的100個DealCharge,Hibernate會對DealCharge做100次刪除。
如果將“on-delete”修改為“cascade”,Hibernate不會執(zhí)行DealCharge的刪除動作;而是讓數(shù)據(jù)庫根據(jù)ON CASCADE DELETE約束自動刪除那100個DealCharge。不過,需要讓DBA開啟ON CASCADE DELETE約束,大多數(shù)DBA不愿意這么做,因為他們想避免父對象的意外刪除級聯(lián)到它的依賴對象上。此外,還要注意,該特性會繞過Hibernate對版本數(shù)據(jù)(versioned data)的常用樂觀鎖策略。
4.10.5 增強的序列標(biāo)識符生成器
范例11中使用Oracle的序列作為標(biāo)識符生成器。假設(shè)我們保存100個ElectricityDeal,Hibernate會將下面的SQL語句執(zhí)行100次來獲取下一個可用的標(biāo)識符:
select SEQ_ELECTRICITY_DEALS.NEXTVAL from dual;
如果網(wǎng)絡(luò)不是很快,那這無疑會降低效率。3.2.3及后續(xù)版本中增加了一個增強的生成器“SequenceStyleGenerator”,它帶了兩個優(yōu)化器:hilo和pooled。盡管HRD的第5章“基礎(chǔ)O/R映射” 講到了這兩個優(yōu)化器,不過內(nèi)容有限。兩個優(yōu)化器都使用了HiLo算法,該算法生成的標(biāo)識符等于Hi值加上Lo值,其中Hi值代表組號,Lo值順序且重復(fù)地從1迭代到最大組大小,組號在Lo值“轉(zhuǎn)回到”1時加1。
假設(shè)組大小是5(可以用max_lo或increment_size參數(shù)來表示),下面是個例子:

直到內(nèi)存組中的值耗盡后,兩個優(yōu)化器才會去訪問數(shù)據(jù)庫,上面的例子每5個標(biāo)識值符訪問一次數(shù)據(jù)庫。使用hilo優(yōu)化器時,你的序列不能再被其他應(yīng)用程序使用,除非它們使用與Hibernate相同的邏輯。使用pooled優(yōu)化器,在其他應(yīng)用程序使用同一序列時則相當(dāng)安全。
兩個優(yōu)化器都有一個問題,如果Hibernate崩潰,當(dāng)前組內(nèi)的一些標(biāo)識符值就會丟失,然而大多數(shù)應(yīng)用程序都不要求擁有連續(xù)的標(biāo)識符值(如果你的數(shù)據(jù)庫,比方說Oracle,緩存了序列值,當(dāng)它崩潰時你也會丟失標(biāo)識符值)。
如果在范例11中使用pooled優(yōu)化器,新的id配置如下:
<id name="key" column="ID">
<generator class="org.hibernate.id.enhance.SequenceStyleGenerator">
<param name="sequence_name">SEQ_ELECTRICITY_DEALS</param>
<param name="initial_value">0</param>
<param name="increment_size">100</param>
<param name="optimizer ">pooled</param>
</generator>
</id>
5 總結(jié)
本文涵蓋了大多數(shù)你在Hibernate應(yīng)用程序調(diào)優(yōu)時會覺得很有用的調(diào)優(yōu)技巧,其中的大多數(shù)時間都在討論那些行之有效卻缺乏文檔的調(diào)優(yōu)主題,例如繼承映射、二級緩存和增強的序列標(biāo)識符生成器。
它還提到了一些Hibernate調(diào)優(yōu)所必需的數(shù)據(jù)庫知識。一些范例中包含了你可能遇到的問題的實際解決方案。
除此之外,值得一提的是Hibernate也可以和In-Memory Data Grid(IMDG)一起使用,例如Oracle的Coherance或GigaSpaces IMDG,這能讓你的應(yīng)用程序達(dá)到毫秒級別