hibernate實際開發中用到的東西,其缺點和優點

langgufu314發表於2011-08-17
[color=blue][size=medium]spring在管理hibernate上有獨到的地方可以順手拿來用,我也是想在能不拋棄hibernate的基礎上儘可能多挖掘一下它的一些效能提升上的做法,總結大家的看法,基本得出一致結論:複雜查詢依靠jdbc的sql或者hibernate提供的本地化sql封裝,或者使用spring的管理,都可以提升效能和效率.我認為對於hibernate的效能優化應該是無止境的.基於大家對此的看法意見和網上的一些資料,加上自己的測試,收錄如下,望大家多提寶貴意見:


在專案中使用Hibernate進行大資料量的效能測試,有一些總結,
1) 在處理大資料量時,會有大量的資料緩衝儲存在Session的一級快取中,這快取大太時會嚴重顯示效能,所以在使用Hibernate處理大資料量的,可以使用session.clear()或者session. Evict(Object) 在處理過程中,清除全部的快取或者清除某個物件。
2) 對大資料量查詢時,慎用list()或者iterator()返回查詢結果,
1. 使用List()返回結果時,Hibernate會所有查詢結果初始化為持久化物件,結果集較大時,會佔用很多的處理時間。
2. 而使用iterator()返回結果時,在每次呼叫iterator.next()返回物件並使用物件時,Hibernate才呼叫查詢將對應的物件初始化,對於大資料量時,每呼叫一次查詢都會花費較多的時間。當結果集較大,但是含有較大量相同的資料,或者結果集不是全部都會使用時,使用iterator()才有優勢。
3. 對於大資料量,使用qry.scroll()可以得到較好的處理速度以及效能。而且直接對結果集向前向後滾動。
3) 對於關聯操作,Hibernate雖然可以表達複雜的資料關係,但請慎用,使資料關係較為簡單時會得到較好的效率,特別是較深層次的關聯時,效能會很差。
4) 對含有關聯的PO(持久化物件)時,若default-cascade= "all "或者 “save-update”,新增PO時,請注意對PO中的集合的賦值操作,因為有可能使得多執行一次update操作。
5) 在一對多、多對一的關係中,使用延遲載入機制,會使不少的物件在使用時方會初始化,這樣可使得節省記憶體空間以及減少的負荷,而且若PO中的集合沒有被使用時,就可減少互資料庫的互動從而減少處理時間。 資料庫

什麼叫n+1次select查詢問題?

在Session的快取中存放的是相互關聯的物件圖。預設情況下,當Hibernate從資料庫中載入Customer物件時,會同時載入所有關聯的Order物件。以Customer和Order類為例,假定ORDERS表的CUSTOMER_ID外來鍵允許為null,圖1列出了CUSTOMERS表和ORDERS表中的記錄。


以下Session的find()方法用於到資料庫中檢索所有的Customer物件:

List customerLists=session.find( "from Customer as c ");

執行以上find()方法時,Hibernate將先查詢CUSTOMERS表中所有的記錄,然後根據每條記錄的ID,到ORDERS表中查詢有參照關係的記錄,Hibernate將依次執行以下select語句:

select * from CUSTOMERS;
select * from ORDERS where CUSTOMER_ID=1;
select * from ORDERS where CUSTOMER_ID=2;
select * from ORDERS where CUSTOMER_ID=3;
select * from ORDERS where CUSTOMER_ID=4;

通過以上5條select語句,Hibernate最後載入了4個Customer物件和5個Order物件,在記憶體中形成了一幅關聯的物件圖,參見圖2。


Hibernate在檢索與Customer關聯的Order物件時,使用了預設的立即檢索策略。這種檢索策略存在兩大不足:

(a) select語句的數目太多,需要頻繁的訪問資料庫,會影響檢索效能。如果需要查詢n個Customer物件,那麼必須執行n+1次select查詢語句。這就是經典的n+1次select查詢問題。這種檢索策略沒有利用SQL的連線查詢功能,例如以上5條select語句完全可以通過以下1條select語句來完成:

select * from CUSTOMERS left outer join ORDERS
on CUSTOMERS.ID=ORDERS.CUSTOMER_ID

以上select語句使用了SQL的左外連線查詢功能,能夠在一條select語句中查詢出CUSTOMERS表的所有記錄,以及匹配的ORDERS表的記錄。

(b)在應用邏輯只需要訪問Customer物件,而不需要訪問Order物件的場合,載入Order物件完全是多餘的操作,這些多餘的Order物件白白浪費了許多記憶體空間。
為了解決以上問題,Hibernate提供了其他兩種檢索策略:延遲檢索策略和迫切左外連線檢索策略。延遲檢索策略能避免多餘載入應用程式不需要訪問的關聯物件,迫切左外連線檢索策略則充分利用了SQL的外連線查詢功能,能夠減少select語句的數目。

剛查閱了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查詢。

通常情況下,我們並不使用對映文件進行抓取策略的定製。更多的是,保持其預設值,然後在特定的事務中, 使用HQL的左連線抓取(left join fetch) 對其進行過載。這將通知 Hibernate在第一次查詢中使用外部關聯(outer join),直接得到其關聯資料。 在條件查詢 API中,應該呼叫 setFetchMode(FetchMode.JOIN)語句。


6) 對於大資料量新增、修改、刪除操作或者是對大資料量的查詢,與資料庫的互動次數是決定處理時間的最重要因素,減少互動的次數是提升效率的最好途徑,所以在開發過程中,請將show_sql設定為true,深入瞭解Hibernate的處理過程,嘗試不同的方式,可以使得效率提升。
7) Hibernate是以JDBC為基礎,但是Hibernate是對JDBC的優化,其中使用Hibernate的緩衝機制會使效能提升,如使用二級快取以及查詢快取,若命中率較高明,效能會是到大幅提升。
8) Hibernate可以通過設定hibernate.jdbc.fetch_size,hibernate.jdbc.batch_size等屬性,對Hibernate進行優化。

hibernate.jdbc.fetch_size 50

hibernate.jdbc.batch_size 25

這兩個選項非常非常非常重要!!!將嚴重影響Hibernate的CRUD效能!

C = create, R = read, U = update, D = delete

Fetch Size 是設定JDBC的Statement讀取資料的時候每次從資料庫中取出的記錄條數。

例如一次查詢1萬條記錄,對於Oracle的JDBC驅動來說,是不會1次性把1萬條取出來的,而只會取出Fetch Size條數,當紀錄集遍歷完了這些記錄以後,再去資料庫取Fetch Size條資料。

因此大大節省了無謂的記憶體消耗。當然Fetch Size設的越大,讀資料庫的次數越少,速度越快;Fetch Size越小,讀資料庫的次數越多,速度越慢。

這有點像平時我們寫程式寫硬碟檔案一樣,設立一個Buffer,每次寫入Buffer,等Buffer滿了以後,一次寫入硬碟,道理相同。

Oracle資料庫的JDBC驅動預設的Fetch Size=10,是一個非常保守的設定,根據我的測試,當Fetch Size=50的時候,效能會提升1倍之多,當Fetch Size=100,效能還能繼續提升20%,Fetch Size繼續增大,效能提升的就不顯著了。

因此我建議使用Oracle的一定要將Fetch Size設到50。

不過並不是所有的資料庫都支援Fetch Size特性,例如MySQL就不支援。

MySQL就像我上面說的那種最壞的情況,他總是一下就把1萬條記錄完全取出來,記憶體消耗會非常非常驚人!這個情況就沒有什麼好辦法了 :(

Batch Size是設定對資料庫進行批量刪除,批量更新和批量插入的時候的批次大小,有點相當於設定Buffer緩衝區大小的意思。

Batch Size越大,批量操作的向資料庫傳送sql的次數越少,速度就越快。我做的一個測試結果是當Batch Size=0的時候,使用Hibernate對Oracle資料庫刪除1萬條記錄需要25秒,Batch Size = 50的時候,刪除僅僅需要5秒!!!

//
我們通常不會直接操作一個物件的識別符號(identifier),因此識別符號的setter方法應該被宣告為私有的(private)。這樣當一個物件被儲存的時候,只有Hibernate可以為它分配識別符號。你會發現Hibernate可以直接訪問被宣告為public,private和protected等不同級別訪問控制的方法(accessor method)和欄位(field)。 所以選擇哪種方式來訪問屬性是完全取決於你,你可以使你的選擇與你的程式設計相吻合。

所有的持久類(persistent classes)都要求有無參的構造器(no-argument constructor);因為Hibernate必須要使用Java反射機制(Reflection)來例項化物件。構造器(constructor)的訪問控制可以是私有的(private),然而當生成執行時代理(runtime proxy)的時候將要求使用至少是package級別的訪問控制,這樣在沒有位元組碼編入(bytecode instrumentation)的情況下,從持久化類裡獲取資料會更有效率一些。



hibernate.max_fetch_depth 設定外連線抓取樹的最大深度
取值. 建議設定為0到3之間


就是每次你在查詢時,會級聯查詢的深度,譬如你對關聯vo設定了eager的話,如果fetch_depth值太小的話,會發多很多條sql



Hibernate的Reference之後,可以採用批量處理的方法,當插入的資料超過10000時,就flush session並且clear。

下面是一個測試method。

1 /** */ /**
2 * 測試成批插入資料的事務處理,返回是否成功
3 *
4 * @param objPO Object
5 * @return boolean
6 */
7 public boolean insertBatch( final Object objPO) {
8 boolean isSuccess = false ;
9 Transaction transaction = null ;
10 Session session = openSession();
11 try {
12 transaction = session.beginTransaction();
13 for ( int i = 0 ; i < 100000 ; i ++ ) {
14 session.save(objPO);
15 if (i % 50 == 0 ) {
16 // flush a batch of inserts and release memory
17 session.flush();
18 session.clear();
19 }
20 }
21 transaction.commit();
22 logger.info( " transaction.wasCommitted: "
23 + transaction.wasCommitted());
24 isSuccess = true ;
25 } catch (HibernateException ex) {
26 if (transaction != null ) {
27 try {
28 transaction.rollback();
29 logger.error( " transaction.wasRolledBack: "
30 + transaction.wasRolledBack());
31 } catch (HibernateException ex1) {
32 logger.error(ex1.getMessage());
33 ex1.printStackTrace();
34 }
35 }
36 logger.error( " Insert Batch PO Error: " + ex.getMessage());
37 ex.printStackTrace();
38 } finally {
39 if (transaction != null ) {
40 transaction = null ;
41 }
42 session.close();
43 }
44 return isSuccess;
45 }
46

這只是簡單的測試,實際專案中遇到的問題,要比這個複雜得多。
這時候,我們可以讓Spring來控制Transaction,自己來控制Hibernate的Session,隨時更新資料。
首先,利用HibernateDaoSupport類來自定義個方法開啟Session;

1public Session openSession(){
2
3 return getHibernateTemplate().getSessionFactory().openSession();
4
5 }

然後,用開啟的Session處理你的資料;

1protected void doBusiness(Session session) {
2
3 while (true) {
4 //do your business with the opening session
5 someMethod(session);
6 session.flush();
7 session.clear();
8 logger.info( "good job! ");
9 }
10}

每做一次資料操作,就更新一次Session,這樣可以保證每次資料操作都成功,否則就讓Spring去控制它roll back吧。
最後,記得關閉Session。

1 Session session = openSession();
2 doBusiness(session);
3 session.close(); // 關閉session [/size][/color]

相關文章