Hibernate提高性能

導讀:
  20.1. 抓取策略(Fetching strategies)
  抓取策略(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語句)。不要混淆它們!咱們使用抓取來改善性能。咱們使用延遲來定義一些契約,對某特定類的某個脫管的實例,知道有哪些數據是可使用的。
  20.1.1. 操做延遲加載的關聯
  默認狀況下,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中,具體選擇哪一種抓取策略的機制是和選擇 單值關聯或集合關聯相一致的。
  20.1.2. 調整抓取策略(Tuning fetch strategies)
  查詢抓取(默認的)在N+1查詢的狀況下是極其脆弱的,所以咱們可能會要求在映射文檔中定義使用鏈接抓取:
  fetch="join">  fetch="join">
  
  
   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次查詢的方法是,使用二級緩存。
  20.1.3. 單端關聯代理(Single-ended association proxies)
  在Hinerbate中,對集合的延遲抓取的採用了本身的實現方法。可是,對於單端關聯的延遲抓取,則須要採用 其餘不一樣的機制。單端關聯的目標實體必須使用代理,Hihernate在運行期二進制級(經過優異的CGLIB庫), 爲持久對象實現了延遲載入代理。
  默認的,Hibernate3將會爲全部的持久對象產生代理(在啓動階段),而後使用他們實現 多對一(many-to-one)關聯和一對一(one-to-one)關聯的延遲抓取。
  在映射文件中,能夠經過設置proxy屬性爲目標class聲明一個接口供代理接口使用。 默認的,Hibernate將會使用該類的一個子類。 注意:被代理的類必須實現一個至少包可見的默認構造函數,咱們建議全部的持久類都應擁有這樣的構造函數
  在如此方式定義一個多態類的時候,有許多值得注意的常見性的問題,例如:
  
  ......
  
  .....
  
  
  首先,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的單根繼承模型的天生限制。若是你但願避免這些問題,那麼你的每一個持久化類必須實現一個接口, 在此接口中已經聲明瞭其業務方法。而後,你須要在映射文檔中再指定這些接口。例如:
  
  ......
  
  .....
  
  
  這裏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()方法的持久化類。
  20.1.4. 實例化集合和代理(Initializing collections and proxies)
  在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();
  20.1.5. 使用批量抓取(Using batch fetching)
  Hibernate能夠充分有效的使用批量抓取,也就是說,若是僅一個訪問代理(或集合),那麼Hibernate將不載入其餘未實例化的代理。 批量抓取是延遲查詢抓取的優化方案,你能夠在兩種批量抓取方案之間進行選擇:在類級別和集合級別。
  類/實體級別的批量抓取很容易理解。假設你在運行時將須要面對下面的問題:你在一個Session中載入了25個 Cat實例,每一個Cat實例都擁有一個引用成員owner, 其指向Person,而Person類是代理,同時lazy="true"。 若是你必須遍歷整個cats集合,對每一個元素調用getOwner()方法,Hibernate將會默認的執行25次SELECT查詢, 獲得其owner的代理對象。這時,你能夠經過在映射文件的Person屬性,顯式聲明batch-size,改變其行爲:
  ...
  隨之,Hibernate將只須要執行三次查詢,分別爲十、十、 5。
  你也能夠在集合級別定義批量抓取。例如,若是每一個Person都擁有一個延遲載入的Cats集合, 如今,Sesssion中載入了10個person對象,遍歷person集合將會引發10次SELECT查詢, 每次查詢都會調用getCats()方法。若是你在Person的映射定義部分,容許對cats批量抓取, 那麼,Hibernate將能夠預先抓取整個集合。請看例子:
  
  
  ...
  
  
  若是整個的batch-size是3(筆誤?),那麼Hibernate將會分四次執行SELECT查詢, 按照三、三、三、1的大小分別載入數據。這裏的每次載入的數據量還具體依賴於當前Session中未實例化集合的個數。
  若是你的模型中有嵌套的樹狀結構,例如典型的賬單-原料結構(bill-of-materials pattern),集合的批量抓取是很是有用的。 (儘管在更多狀況下對樹進行讀取時,嵌套集合(nested set)或原料路徑(materialized path)() 是更好的解決方法。)
  20.1.6. 使用子查詢抓取(Using subselect fetching)
  倘若一個延遲集合或單值代理須要抓取,Hibernate會使用一個subselect從新運行原來的查詢,一次性讀入全部的實例。這和批量抓取的實現方法是同樣的,不會有破碎的加載。
  20.1.7. 使用延遲屬性抓取(Using lazy property fetching)
  Hibernate3對單獨的屬性支持延遲抓取,這項優化技術也被稱爲組抓取(fetch groups)。 請注意,該技術更多的屬於市場特性。在實際應用中,優化行讀取比優化列讀取更重要。可是,僅載入類的部分屬性在某些特定狀況下會有用,例如在原有表中擁有幾百列數據、數據模型沒法改動的狀況下。
  能夠在映射文件中對特定的屬性設置lazy,定義該屬性爲延遲載入。
  
  
  
  
  
  
  
  
  屬性的延遲載入要求在其代碼構建時加入二進制指示指令(bytecode instrumentation),若是你的持久類代碼中未含有這些指令, Hibernate將會忽略這些屬性的延遲設置,仍然將其直接載入。
  你能夠在Ant的Task中,進行以下定義,對持久類代碼加入「二進制指令。」
  
  
  
  
  
  
  
  
  
  
  
  
  還有一種能夠優化的方法,它使用HQL或條件查詢的投影(projection)特性,能夠避免讀取非必要的列, 這一點至少對只讀事務是很是有用的。它無需在代碼構建時「二進制指令」處理,所以是一個更加值得選擇的解決方法。
  有時你須要在HQL中經過抓取全部屬性,強行抓取全部內容。
  20.2. 二級緩存(The Second Level Cache)
  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.)
  20.2.1. 緩存映射(Cache mappings)
  類或者集合映射的「元素」能夠有下列形式:
  usage="transactional|read-write|nonstrict-read-write|read-only" (1)
/>  usage="transactional|read-write|nonstrict-read-write|read-only" (1)
  />
  (1) usage說明了緩存的策略: transactional、 read-write、 nonstrict-read-write或 read-only。
  另外(首選?), 你能夠在hibernate.cfg.xml中指定和 元素。
  這裏的usage屬性指明瞭緩存併發策略(cache concurrency strategy)。
  20.2.2. 策略:只讀緩存(Strategy: read only)
  若是你的應用程序只需讀取一個持久化類的實例,而無需對其修改, 那麼就能夠對其進行只讀緩存。這是最簡單,也是實用性最好的方法。甚至在集羣中,它也能完美地運做。
  
  
  ....
  
  20.2.3. 策略:讀/寫緩存(Strategy: read/write)
  若是應用程序須要更新數據,那麼使用讀/寫緩存比較合適。 若是應用程序要求「序列化事務」的隔離級別(serializable transaction isolation level),那麼就決不能使用這種緩存策略。 若是在JTA環境中使用緩存,你必須指定hibernate.transaction.manager_lookup_class屬性的值, 經過它,Hibernate才能知道該應用程序中JTA的TransactionManager的具體策略。 在其它環境中,你必須保證在Session.close()、或Session.disconnect()調用前, 整個事務已經結束。 若是你想在集羣環境中使用此策略,你必須保證底層的緩存實現支持鎖定(locking)。Hibernate內置的緩存策略並不支持鎖定功能。
  
  
  ....
  
  
  ....
  
  
  20.2.4. 策略:非嚴格讀/寫緩存(Strategy: nonstrict read/write)
  若是應用程序只偶爾須要更新數據(也就是說,兩個事務同時更新同一記錄的狀況很不常見),也不須要十分嚴格的事務隔離, 那麼比較適合使用非嚴格讀/寫緩存策略。若是在JTA環境中使用該策略, 你必須爲其指定hibernate.transaction.manager_lookup_class屬性的值, 在其它環境中,你必須保證在Session.close()、或Session.disconnect()調用前, 整個事務已經結束。
  20.2.5. 策略:事務緩存(transactional)
  Hibernate的事務緩存策略提供了全事務的緩存支持, 例如對JBoss TreeCache的支持。這樣的緩存只能用於JTA環境中,你必須指定 爲其hibernate.transaction.manager_lookup_class屬性。
  沒有一種緩存提供商可以支持上列的全部緩存併發策略。下表中列出了各類提供器、及其各自適用的併發策略。
  表 20.2. 各類緩存提供商對緩存併發策略的支持狀況(Cache Concurrency Strategy Support)
  Cache read-only nonstrict-read-write read-write transactional
  Hashtable (not intended for production use) yes yes yes
  EHCache yes yes yes
  OSCache yes yes yes
  SwarmCache yes yes
  JBoss TreeCache yes yes
  20.3. 管理緩存(Managing the caches)
  不管什麼時候,當你給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
  20.4. 查詢緩存(The Query Cache)
  查詢的結果集也能夠被緩存。只有當常用一樣的參數進行查詢時,這纔會有些用處。 要使用查詢緩存,首先你必須打開它:
  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()的更爲有效的替代方案,一樣能夠清除查詢緩存區域。
  20.5. 理解集合性能(Understanding Collection performance)
  前面咱們已經對集合進行了足夠的討論。本段中,咱們將着重講述集合在運行時的事宜。
  20.5.1. 分類(Taxonomy)
  Hibernate定義了三種基本類型的集合:
  值數據集合
  一對多關聯
  多對多關聯
  這個分類是區分了不一樣的表和外鍵關係類型,可是它沒有告訴咱們關係模型的全部內容。 要徹底理解他們的關係結構和性能特色,咱們必須同時考慮「用於Hibernate更新或刪除集合行數據的主鍵的結構」。 所以獲得了以下的分類:
  有序集合類
  集合(sets)
  包(bags)
  全部的有序集合類(maps, lists, arrays)都擁有一個由和 組成的主鍵。 這種狀況下集合類的更新是很是高效的——主鍵已經被有效的索引,所以當Hibernate試圖更新或刪除一行時,能夠迅速找到該行數據。
  集合(sets)的主鍵由和其餘元素字段構成。 對於有些元素類型來講,這很低效,特別是組合元素或者大文本、大二進制字段; 數據庫可能沒法有效的對複雜的主鍵進行索引。 另外一方面,對於一對多、多對多關聯,特別是合成的標識符來講,集合也能夠達到一樣的高效性能。( 附註:若是你但願SchemaExport爲你的建立主鍵, 你必須把全部的字段都聲明爲not-null="true"。)
  映射定義了代理鍵,所以它老是能夠很高效的被更新。事實上, 擁有着最好的性能表現。
  Bag是最差的。由於bag容許重複的元素值,也沒有索引字段,所以不可能定義主鍵。 Hibernate沒法判斷出重複的行。當這種集合被更改時,Hibernate將會先完整地移除 (經過一個(in a single DELETE))整個集合,而後再從新建立整個集合。 所以Bag是很是低效的。
  請注意:對於一對多關聯來講,「主鍵」極可能並非數據庫表的物理主鍵。 但就算在此狀況下,上面的分類仍然是有用的。(它仍然反映了Hibernate在集合的各數據行中是如何進行「定位」的。)
  20.5.2. Lists, maps 和sets用於更新效率最高
  根據咱們上面的討論,顯然有序集合類型和大多數set均可以在增長、刪除、修改元素中擁有最好的性能。
  可論證的是對於多對多關聯、值數據集合而言,有序集合類比集合(set)有一個好處。由於Set的內在結構, 若是「改變」了一個元素,Hibernate並不會更新(UPDATE)這一行。 對於Set來講,只有在插入(INSERT)和刪除(DELETE)操做時「改變」纔有效。再次強調:這段討論對「一對多關聯」並不適用。
  注意到數組沒法延遲載入,咱們能夠得出結論,list, map和idbags是最高效的(非反向)集合類型,set則緊隨其後。 在Hibernate中,set應該時最通用的集合類型,這時由於「set」的語義在關係模型中是最天然的。
  可是,在設計良好的Hibernate領域模型中,咱們一般能夠看到更多的集合事實上是帶有inverse="true"的一對多的關聯。對於這些關聯,更新操做將會在多對一的這一端進行處理。所以對於此類狀況,無需考慮其集合的更新性能。
  20.5.3. Bag和list是反向集合類中效率最高的
  在把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();
  20.5.4. 一次性刪除(One shot delete)
  偶爾的,逐個刪除集合類中的元素是至關低效的。Hibernate並沒那麼笨, 若是你想要把整個集合都刪除(好比說調用list.clear()),Hibernate只須要一個DELETE就搞定了。
  假設咱們在一個長度爲20的集合類中新增長了一個元素,而後再刪除兩個。 Hibernate會安排一條INSERT語句和兩條DELETE語句(除非集合類是一個bag)。 這固然是顯而易見的。
  可是,假設咱們刪除了18個數據,只剩下2個,而後新增3個。則有兩種處理方式:
  逐一的刪除這18個數據,再新增三個;
  刪除整個集合類(只用一句DELETE語句),而後增長5個數據。
  Hibernate還沒那麼聰明,知道第二種選擇可能會比較快。 (也許讓Hibernate不這麼聰明也是好事,不然可能會引起意外的「數據庫觸發器」之類的問題。)
  幸運的是,你能夠強制使用第二種策略。你須要取消原來的整個集合類(解除其引用), 而後再返回一個新的實例化的集合類,只包含須要的元素。有些時候這是很是有用的。
  顯然,一次性刪除並不適用於被映射爲inverse="true"的集合。
  20.6. 監測性能(Monitoring performance)
  沒有監測和性能參數而進行優化是毫無心義的。Hibernate爲其內部操做提供了一系列的示意圖,所以能夠從 每一個SessionFactory抓取其統計數據。
  20.6.1. 監測SessionFactory
  你能夠有兩種方式訪問SessionFactory的數據記錄,第一種就是本身直接調用 sessionFactory.getStatistics()方法讀取、顯示統計數據。
  此外,若是你打開StatisticsServiceMBean選項,那麼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級別)其總結。
  20.6.2. 數據記錄(Metrics)
  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
  本文依照HIBERNATE幫助文檔,一些網絡書籍及項目經驗整理而成,只提供要點和思路,具體作法能夠留言探討,或是找一些更詳細更有針對性的資料。
  初用HIBERNATE的人也許都遇到過性能問題,實現同一功能,用HIBERNATE與用JDBC性能相差十幾倍很正常,若是不及早調整,極可能影響整個項目的進度。
  大致上,對於HIBERNATE性能調優的主要考慮點以下:
  數據庫設計調整
  HQL優化
  API的正確使用(如根據不一樣的業務類型選用不一樣的集合及查詢API)
  主配置參數(日誌,查詢緩存,fetch_size, batch_size等)
  映射文件優化(ID生成策略,二級緩存,延遲加載,關聯優化)
  一級緩存的管理
  針對二級緩存,還有許多特有的策略
  事務控制策略。
  一、 數據庫設計
  a) 下降關聯的複雜性
  b) 儘可能不使用聯合主鍵
  c) ID的生成機制,不一樣的數據庫所提供的機制並不徹底同樣
  d) 適當的冗餘數據,不過度追求高範式
  二、 HQL優化
  HQL若是拋開它同HIBERNATE自己一些緩存機制的關聯,HQL的優化技巧同普通的SQL優化技巧同樣,能夠很容易在網上找到一些經驗之談。
  三、 主配置
  a) 查詢緩存,同下面講的緩存不太同樣,它是針對HQL語句的緩存,即徹底同樣的語句再次執行時能夠利用緩存數據。可是,查詢緩存在一個交易系統(數據變動頻繁,查詢條件相同的機率並不大)中可能會起副作用:它會白白耗費大量的系統資源但卻難以派上用場。
  b) fetch_size,同JDBC的相關參數做用相似,參數並非越大越好,而應根據業務特徵去設置
  c) batch_size同上。
  d) 生產系統中,切記要關掉SQL語句打印。
  四、 緩存
  a) 數據庫級緩存:這級緩存是最高效和安全的,但不一樣的數據庫可管理的層次並不同,好比,在ORACLE中,能夠在建表時指定將整個表置於緩存當中。
  b) SESSION緩存:在一個HIBERNATE SESSION有效,這級緩存的可干預性不強,大多於HIBERNATE自動管理,但它提供清除緩存的方法,這在大批量增長/更新操做是有效的。好比,同時增長十萬條記錄,按常規方式進行,極可能會發現OutofMemeroy的異常,這時可能須要手動清除這一級緩存:Session.evict以及Session.clear
  c) 應用緩存:在一個SESSIONFACTORY中有效,所以也是優化的重中之重,所以,各種策略也考慮的較多,在將數據放入這一級緩存以前,須要考慮一些前提條件:
  i. 數據不會被第三方修改(好比,是否有另外一個應用也在修改這些數據?)
  ii. 數據不會太大
  iii. 數據不會頻繁更新(不然使用CACHE可能拔苗助長)
  iv. 數據會被頻繁查詢
  v. 數據不是關鍵數據(如涉及錢,安全等方面的問題)。
  緩存有幾種形式,能夠在映射文件中配置:read-only(只讀,適用於不多變動的靜態數據/歷史數據),nonstrict-read-write,read-write(比較廣泛的形式,效率通常),transactional(JTA中,且支持的緩存產品較少)
  d) 分佈式緩存:同c)的配置同樣,只是緩存產品的選用不一樣,在目前的HIBERNATE中可供選擇的很少,oscache, jboss cache,目前的大多數項目,對它們的用於集羣的使用(特別是關鍵交易系統)都持保守態度。在集羣環境中,只利用數據庫級的緩存是最安全的。
  五、 延遲加載
  a) 實體延遲加載:經過使用動態代理實現
  b) 集合延遲加載:經過實現自有的SET/LIST,HIBERNATE提供了這方面的支持
  c) 屬性延遲加載:
  六、 方法選用
  a) 完成一樣一件事,HIBERNATE提供了可供選擇的一些方式,但具體使用什麼方式,可能用性能/代碼都會有影響。顯示,一次返回十萬條記錄(List/Set/Bag/Map等)進行處理,極可能致使內存不夠的問題,而若是用基於遊標(ScrollableResults)或Iterator的結果集,則不存在這樣的問題。
  b) Session的load/get方法,前者會使用二級緩存,然後者則不使用。
  c) Query和list/iterator,若是去仔細研究一下它們,你可能會發現不少有意思的狀況,兩者主要區別(若是使用了Spring,在HibernateTemplate中對應find,iterator方法):
  i. list只能利用查詢緩存(但在交易系統中查詢緩存做用不大),沒法利用二級緩存中的單個實體,但list查出的對象會寫入二級緩存,但它通常只生成較少的執行SQL語句,不少狀況就是一條(無關聯)。
  ii. iterator則能夠利用二級緩存,對於一條查詢語句,它會先從數據庫中找出全部符合條件的記錄的ID,再經過ID去緩存找,對於緩存中沒有的記錄,再構造語句從數據庫中查出,所以很容易知道,若是緩存中沒有任何符合條件的記錄,使用iterator會產生N+1條SQL語句(N爲符合條件的記錄數)
  iii. 經過iterator,配合緩存管理API,在海量數據查詢中能夠很好的解決內存問題,如:
  while(it.hasNext()){
  YouObject object = (YouObject)it.next();
  session.evict(youObject);
  sessionFactory.evice(YouObject.class, youObject.getId());
  }
  若是用list方法,極可能就出OutofMemory錯誤了。
  iv. 經過上面的說明,我想你應該知道如何去使用這兩個方法了。
  七、 集合的選用
  在HIBERNATE 3.1文檔的「19.5. Understanding Collection performance」中有詳細的說明。
  八、 事務控制
  事務方面對性能有影響的主要包括:事務方式的選用,事務隔離級別以及鎖的選用
  a) 事務方式選用:若是不涉及多個事務管理器事務的話,不須要使用JTA,只有JDBC的事務控制就能夠。
  b) 事務隔離級別:參見標準的SQL事務隔離級別
  c) 鎖的選用:悲觀鎖(通常由具體的事務管理器實現),對於長事務效率低,但安全。樂觀鎖(通常在應用級別實現),如在HIBERNATE中能夠定義VERSION字段,顯然,若是有多個應用操做數據,且這些應用不是用同一種樂觀鎖機制,則樂觀鎖會失效。所以,針對不一樣的數據應有不一樣的策略,同前面許多狀況同樣,不少時候咱們是在效率與安全/準確性上找一個平衡點,不管如何,優化都不是一個純技術的問題,你應該對你的應用和業務特徵有足夠的瞭解。
  九、 批量操做
  即便是使用JDBC,在進行大批數據更新時,BATCH與不使用BATCH有效率上也有很大的差異。咱們能夠經過設置batch_size來讓其支持批量操做。
  舉個例子,要批量刪除某表中的對象,如「delete Account」,打出來的語句,會發現HIBERNATE找出了全部ACCOUNT的ID,再進行刪除,這主要是爲了維護二級緩存,這樣效率確定高不了,在後續的版本中增長了bulk delete/update,但這也沒法解決緩存的維護問題。也就是說,因爲有了二級緩存的維護問題,HIBERNATE的批量操做效率並不盡如人意!
  從前面許多要點能夠看出,不少時候咱們是在效率與安全/準確性上找一個平衡點,不管如何,優化都不是一個純技術的問題,你應該對你的應用和業務特徵有足夠的瞭解,通常的,優化方案應在架構設計期就基本肯定,不然可能致使不必的返工,導致項目延期,而做爲架構師和項目經理,還要面對開發人員可能的抱怨,必竟,咱們對用戶需求更改的控制力不大,但技術/架構風險是應該在初期意識到並制定好相關的對策。
  還有一點要注意,應用層的緩存只是錦上添花,永遠不要把它當救命稻草,應用的根基(數據庫設計,算法,高效的操做語句,恰當API的選擇等)纔是最重要的。

web

相關文章
相關標籤/搜索