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