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

數據保存:

1)session.save

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

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

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

示例:

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萬個用戶數據。那麼,對應我們實現了相應的數據批量導入方法:

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內部緩存,如:

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的改進實現:

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的數量:

<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參數對於批量插入操作的實際影響。

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批量刪除代碼:

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

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

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方法的實現也正是如此,如下:

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批量刪除示例:

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代碼測試:

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表中的所有記錄:

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實際上仍然沒有解決緩存同步上的問題,無法保證緩存數據的一致有效性。
看以下示例:

//加載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刪除的形式:

//加載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二級緩存,運行以下代碼:

//加載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只是提供了面向高性能批量操作的一種實現途徑,但無法保證緩存數據的一致有效性,在實際開發中,必須特別注意這一點,在緩存策略的制定上須特別謹慎。
數據的批量更新與批量刪除相關知識點基本相同,就不再贅述。
爲此犧牲的所謂設計上的優雅性,未必就那麼令人惋惜。畢竟對於應用系統的開發而言,爲客戶提供一個滿足需求並且高效穩定的系統纔是第一目標,產品最終能得到用戶的歡迎,纔是真正的優雅。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章