Hibernate深入淺出(九)持久層操作——數據保存&批量操作


 

數據保存:

1)session.save

session.save方法用於實體對象到數據庫的持久化操作。也就是說,session.save方法調用與實體對象所匹配的Insert SQL,將數據插入庫表。

結合一個簡單實例來進行討論:

1
2
3
4
5
TUser user =  new  TUser();
user.setName( "Luna" );
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();

首先,我們創建了一個user對象,並啟動事務,之后調用session.save方法對對象進行保存。

session.save方法中包含了以下幾個主要步驟:

a. 在session內部緩存中尋找待保存對象

內部緩存命中,則認為此數據已經保存(執行過insert操作),實體對象已經處於Persistent狀態,直接返回。
此時,即使數據相對之前狀態已經發生了變化,也將在稍后的事務提交時,由臟數據檢查過程加以判定,並根據判定結果決定是否要執行對應的update操作。

b. 如果實體類實現了Lifecycle接口,則調用待保存對象的onSave方法

c. 如果實體類實現了Validatable接口,則調用其validate方法

d. 調用對應攔截器的Interceptor.onSave方法(如果有的話)

e. 構造Insert SQL,並加以執行

f. 記錄插入成功,user.id屬性被設定為insert操作返回的新記錄id值

g. 將user對象放入內部緩存

這里值得一提的是,save方法並不會把實體對象納入二級緩存,因為通過save方法保存的實體對象,在事務的剩余部分中被修改幾率往往很高,緩存的頻繁更新以及隨之而來的數據同步問題的代價,已經超過了此數據得到重用的可能收益,得不償失。

h. 最后,如果存在級聯關系,對級聯關系進行遞歸處理。

2)session.update

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TUser user =  new  TUser();
user.setName(“Emma”);
//此時user處於Transient狀態
Transaction tx = session.beginTransaction();
session.save(user);
//user對象已經由Hibernate納入管理容器,處於Persistent狀態
tx.commit();
session.close();
//user對象此時狀態為Detached,因為與其關聯的session已經關閉
Transaction tx2 = session2.beginTransaction();
session2.update(user);
//處於Detached狀態的user對象再次借助session2由Hibernate納入管理容器,
//恢復Persistent狀態
user.setName(“Emma_1”);
//由於user對象再次處於Persistent狀態,因此其屬性變更將自動由
//Hibernate固化到數據庫中
tx2.commt();

這里我們通過update方法將一個Detached狀態的對象與session重新關聯起來,從而使之轉變為Persistent狀態。
那么update方法中,到底進行了怎樣的操作完成這一步驟?
a. 首先,根據待更新實體對象的Key,在當前session的內部緩存中進行查找,如果發現,則認為當前實體對象已經處於Persistent狀態,返回。
從這一點我們可以看出,對一個Persistent狀態的實體對象調用update語句並不會產生任何作用。
b. 初始化實體對象的狀態信息(作為之后臟數據檢查的依據),並將其納入內部緩存。注意這里session.update方法本身並沒有發送Update SQL完成數據更新操作,Update SQL將在之后的session.flush方法中執行(Transaction.commit在真正提交數據庫事務之前會調用session.flush)。

3)session.saveOrUpdate

幕后原理:

a. 首先在session內部緩存中進行查找,如果發現則直接返回。

b. 執行實體類對應的Interceptor.isUnsaved方法(如果有的話),判斷對象是否為未保存狀態。

c. 根據unsave-value判斷對象是否處於未保存狀態。

d. 如果對象未保存(Transient狀態),則調用save方法保存對象。

e. 如果對象已保存(Detached狀態),調用update方法將對象與session重新關聯。

可以看到,saveOrUpdate實際上是save和update方法的組合應用。它本身並沒有增加新的功能特性,但為應用層開發提供了一個為相當邊界的功能選擇。

有了saveOrUpdate方法,處理就相當簡單明了,我們無需關心傳入的user參數到底是怎樣的狀態。

 

數據批量操作:

顯然,最簡單的方式就是通過迭代調用
session.save/update/saveOrUpdate/delete操作。從邏輯上而言,這樣的解決方式並沒有什么問題。不過,從性能角度考慮,這樣的做法卻有待商榷。
1. 數據批量導入

舉個簡單的例子,我們需要導入10萬個用戶數據。那么,對應我們實現了相應的數據批量導入方法:

1
2
3
4
5
6
7
8
9
public  void  importUsers()  throws  HibernateException{
     Transaction tx = session.beginTransaction();
     for ( int  i= 0 ;i< 100000 ;i++){
         TUser user =  new  TUser();
         user.setName(“user”+i);
         session.save(user);
     }
     tx.commit();
}

代碼從邏輯上看並沒有什么問題。但是運行期可能就會發現,程序運行由於OutOfMemoryError而異常中止。
why?原因在於Hibernate內部緩存的維護機制,每次調用
session.save方法時,當前session都會將此對象納入自身的內部緩存進行管理。
內部緩存與二級緩存不同,我們可以在二級緩存的配置中指定其最大容量,但內部緩存並沒有這樣的限制。
隨着循環的進行,越來越多的TUser實例被納入到session內部緩存之中,內存逐漸耗盡,於是產生了OutOfMemoryError。
如何避免這樣的問題?
一個解決方案是每隔一段時間清空session內部緩存,如:

1
2
3
4
5
6
7
8
9
10
11
Transaction tx = session.beginTransaction();
for ( int  i= 0 ;i< 100000 ;i++){
     TUser user =  new  TUser();
     user.setName(“user”+i);
     session.save(user);
     if (i% 25 == 0 ){ //以每25個數據作為一個處理單元
         session.flush();
         session.clear();
     }
}
tx.commit();

在傳統JDBC編程時,對於批量操作,一般用怎樣的方式加以優化?

下面的代碼是一個典型的基於JDBC的改進實現:

1
2
3
4
5
6
PreparedStatement stmt = conn.prepareStatement(“INSERT INTO t_user(name) VALUES(?)”);
for ( int  i= 0 ;i< 100000 ;i++){
     stmt.setString( 1 ,”user”+i);
     stmt.addBatch();
}
int [] counts = stmt.executeBatch();

這里我們通過PreparedStatement.executeBacth方法,將數個SQL操作批量提交以獲得性能上的提升。
那么Hibernate中是否有對應的批量操作方式呢?
我們可以通過設置hibernate.jdbc.batch_size參數來指定Hibernate每次提交SQL的數量:

1
2
3
4
5
6
7
< hibernate-mapping >
     < session-factory >
        
         < property  name=”hibernate.jdbc.batch_size”>25</ property >
        
     </ session-factory >
</ hibernate-mapping >

這樣,當我們發起SQL調用的時候,Hibernate會累積到25個SQL之后批量提交,從而實現了與上面JDBC代碼類似的效能。
同樣的方法,也可以用於Update操作和Delete操作。
下面做個簡單的測試,看看hibernate.jdbc.batch_size參數對於批量插入操作的實際影響。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public  void  importUserList()  throws  HibernateException{
     Transaction tx = session.beginTransaction();
     for ( int  i= 0 ;i< 100000 ;i++){
         TUser user =  new  TUser();
         user.setName(“user”+i);
         session.save(user);
         if (i% 25 == 0 ){ //以每25個數據作為一個處理單元
             session.flush();
             session.clear();
         }
     }
     tx.commit();
}
public  void  testBatchInsert(){
     long  startTime = System.currentTimeMillis();
     try {
         this .importUserList();
     } catch (HibernateException e){
         e.printStackTrace();
     }
     long  currentTime = System.currentTimeMillis();
     System.out.println(“Batch Insert Time cost in ms => “+(currentTime-startTime));
}

測試環境:
操作系統:XP sp2
JDK版本:Sun JDK 1.4.2_08
CPU: p4 1.5G Mobile
RAM:512M
數據庫:SQLServer2000/Oracle9i
JDBC:jtds JDBC Driver for SQLServer 1.02/Oracle JDBC Driver 9.0.2.0.0
注:Mysql JDBC Driver不支持BatchUpdate方式,因此batch_size的設定對MySQL無效。
對於遠程數據庫,hibernate.jdbc.batch_size的設定就相當關鍵。
這里的差距,並不是數據存取機制有什么不同,而是在於網絡傳輸上的損耗,對於數據庫與應用均部署在本機的情況而言,數據通訊上的性能損耗較小,因而hibernate.jdbc.batch_size設定的影響相對較弱,而對於遠程數據庫,網絡傳輸上的損耗就不可不計,因而不同的傳輸模式(批量傳輸與單筆傳輸)將對性能的整體表現產生較大影響。
2. 數據批量刪除

批量刪除操作在Hibernate2和Hibernate3中有着不同的實現機制,首先來看Hibernate2中的批量刪除。
下面是一段典型的Hibernate2批量刪除代碼:

1
2
3
Transaction tx = session.beginTransaction();
session.delete(“from TUser”);
tx.commit();

(假設數據庫t_user表中有1000條記錄)
對於這樣的代碼,Hibernate會執行以下語句:
Hibernate會首先從數據庫查詢出所有符合條件的記錄,再對此記錄進行循環刪除,實際上,session.delete(“from TUser”)等價於:

1
2
3
4
5
6
7
Transaction tx = session.beginTransaction();
List userList = session.find(“from TUser”);
int  len = userList.size();
for ( int  i= 0 ;i<len;i++){
     session.delete(userList.get(i));
}
tx.commit();

實際上,Hibernate內部,Delete方法的實現也正是如此,如下:

1
2
3
4
5
6
7
8
9
10
11
12
public  int  delete(String query, Object[] values, Type[] types)  throws
     HibernateException{
     if (log.isTraceEnabled()){
         log.trace(“delete: ”+query);
         if (values.length!= 0 ) log.trace(“parameters: “+
         StringHelper.toString(values));
     }
     List list = find(query,values,types);
     int  size = list.size();
     for ( int  i= 0 ;i<size;i++) delete(list.get(i));
     return  size;
}

看上去很難以理解的實現方式,為什么Hibernate不單獨執行一條Delete SQL”delete t_user where id>5”完成所有的工作呢?

這就是所有ORM框架都必須面對的問題。ORM為了自動維持其內部狀態屬性,必須知道用戶到底對哪些數據進行了操作。它必須首先從數據中獲得所有待刪除對象,才能根據這些對象,對目前內部緩存和二級緩存中的數據進行整理,以保持內存狀態與數據庫數據的一致性。

當然,解決辦法並不是沒有,ORM可以根據調用的Delete SQL對緩存中的數據進行處理,只要是緩存中TUser對象的id值大於5的統統廢除,緩存數據廢除之后,再執行”delete from t_user where id>5”.但是,如此的需求將導致緩存的管理復雜性大大增加(實際上是實現了一個支持SQL的內存數據庫),這樣的要求對於一個輕量級的ORM實現而言未免苛刻。
批量刪出操作同樣會遇到與數據批量導入操作同樣的問題:
1)    內存消耗

對於內存消耗問題,無法像之前一樣通過session.clear操作解決,因為我們並無法干涉數據的批量加載過程。
變通的方法之一:用session.iterate或者Query.iterate方法逐條獲取數據,再執行delete操作。
另外,Hibernate2.16之后的版本提供了基於游標的數據遍歷操作,為解決這個問題提供了一個較好的解決方案(前提是所使用的JDBC驅動必須支持游標)。通過游標,我們可以逐條獲取數據,從而使得內存處於較為穩定的使用狀態。
下面是基於游標的Hibernate批量刪除示例:

1
2
3
4
5
6
7
8
9
Transaction tx = session.beginTransaction();
String hql = “from TUser”;
Query query = session.createQuery(hql);
ScrollableResults scRes = query.scroll();
while (scRes.next()){
     TUser user = (TUser)scRes.get( 0 );
     session.delete(user);
}
tx.commit();

2)    迭代刪除操作的執行效率

由於Hibernate批量刪除操作過程中,需要反復調用delete  SQL,因此同樣存在SQL批量發送問題,對於這個問題,我們仍采用調整hibernate.jdbc.batch_size參數解決。

使用JDBC代碼測試:

1
2
3
String sqlStr = “delete from t_user”;
Statement statement = dbconn.createStatement();
statement.execute(sqlStr);

耗時:390ms。
可以看到,即使是優化過的批量刪除功能,性能差距還是相當可觀的(近10倍的差距)。因此,在Hibernate2中,對於批量操作而言,適當的時候采用傳統的JDBC進行直接的批量數據庫操作(此時應特別注意對緩存的影響),可以獲得性能上的極大提升,特別是對於批量性能關鍵的邏輯實現而言。
考慮到以上問題,Hibernate3 HQL語法中引入了bulk delete/update操作,bulk delete/update操作的原理,即通過一條獨立的SQL語句完成數據的批量刪除/更新操作(類似上例中的JDBC批量刪除)。
我們可以通過如下代碼刪除t_user表中的所有記錄:

1
2
3
4
5
6
Transaction tx = session.beginTransaction();
String hql = “delete from t_user”;
Query query = session.createQuery(hql);
int  ret = query.executeUpdate();
tx.commit();
System.out.println(“delete records =>”+ret);

觀察運行期日志輸出:

可以看到,通過一條干凈利落的”delete from t_user”語句,我們即完成數據的批量刪除功能,從底層實現來看,這與之前JDBC示例中的實現方式並沒有什么不同,性能表現也大致相似。
那么,我們之前曾談及的批量刪除與緩存管理上的矛盾,在Hibernate3中是否仍然存在?
這也正是必須特別注意的一點,Hibernate3的bulk delete/update實際上仍然沒有解決緩存同步上的問題,無法保證緩存數據的一致有效性。
看以下示例:

1
2
3
4
5
6
7
8
9
10
//加載id=1的用戶記錄
TUser user = (TUser)session.load(TUser. class new  Integer( 1 ));
System.out.println(“User name is ==> “+user.getName());
//刪除id=1的用戶記錄
Transaction tx = session.beginTransaction();
session.delete(user);
tx.commit();
//嘗試再次加載
user = (TUser)session.load(TUser. class new  Integer( 1 ));
System.out.println(“User name is ==> “+user.getName());

嘗試運行以上代碼,在嘗試再次加載已刪除的TUser對象時,Hibernate將拋出ObjectDeletedException,表明此對象已刪除,加載失敗。
將以上代碼修改為通過bulk delete/update刪除的形式:

1
2
3
4
5
6
7
8
9
10
11
12
//加載id=1的用戶記錄
TUser user = (TUser)session.load(TUser. class new  Integer( 1 ));
System.out.println(“User name is ==> “+user.getName());
//通過bulk delete/update刪除id=1的用戶記錄
Transaction tx = session.beginTransaction();
String hql = “delete from t_user where id= 1 ”;
Query query = session.createQuery(hql);
query.executeUpdate();
tx.commit();
//嘗試再次加載
user = (TUser)session.load(TUser. class new  Integer( 1 ));
System.out.println(“User name is ==> “+user.getName());

輸出日志如下:

可以看到,第二次加載操作成功,由於緩存同步上的問題,我們得到了一個已經被刪除的過期數據對象。
通過前面的討論,我們知道,Hibernate中維護了兩級緩存。
上面的代碼中,我們通過同一個session實例反復進行數據加載,第二次查詢操作將從內部緩存中直接查找數據返回。
   那么,在不同session實例之間的協調情況如何,二級緩存中的數據有效性是否能得到保證?
打開Hibernate二級緩存,運行以下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
//加載id=1的用戶記錄
TUser user = (TUser)session.load(TUser. class new  Integer( 1 ));
System.out.println(“User name is ==> “+user.getName());
//加載id=1的用戶記錄已被放入二級緩存
//通過bulk delete/update刪除id=1的用戶記錄
Transaction tx = session.beginTransaction();
String hql = “delete from t_user where id= 1 ”;
Query query = session.createQuery(hql);
query.executeUpdate();
tx.commit();
//通過另一個session實例再次嘗試加載
user = (TUser)anotherSession.load(TUser. class new  Integer( 1 ));
System.out.println(“User name is ==> “+user.getName());

在嘗試再次加載已刪除數據對象時,我們調用了另一個session實例。
運行日志輸出如下:
可以看到,與前例相同,第二次數據加載時Hibernate依然返回了無效數據。
也就是說,bulk delete/update只是提供了面向高性能批量操作的一種實現途徑,但無法保證緩存數據的一致有效性,在實際開發中,必須特別注意這一點,在緩存策略的制定上須特別謹慎。
數據的批量更新與批量刪除相關知識點基本相同,就不再贅述。
為此犧牲的所謂設計上的優雅性,未必就那么令人惋惜。畢竟對於應用系統的開發而言,為客戶提供一個滿足需求並且高效穩定的系統才是第一目標,產品最終能得到用戶的歡迎,才是真正的優雅。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM