9.4 批量處理資料
通常,在一個Session對象的緩衝中只存放數量有限的持久化對象,等到Session對象處理事務完畢,還要關閉Session對象,從而及時釋放Session的緩衝佔用的記憶體。
批量處理資料是指在一個事務中處理大量資料。以下程式在一個事務中批次更新CUSTOMERS表中年齡大於零的所有記錄的AGE欄位:
Transaction tx = session.beginTransaction();
Iterator customers=
session.createQuery("from Customer c where c.age>0").list().iterator();
while(customers.hasNext()){
Customer customer=(Customer)customers.next();
customer.setAge(customer.getAge()+1);
}
tx.commit();
session.close();
如果CUSTOMERS表中有1萬條年齡大於零的記錄,那麼Hibernate會一下子載入1萬個Customer對象到記憶體。當執行tx.commit()方法時,會清理緩衝,Hibernate執行1萬條更新CUSTOMERS表的update語句:
update CUSTOMERS set AGE=? …. where ID=i;
update CUSTOMERS set AGE=? …. where ID=j;
……
update CUSTOMERS set AGE=? …. where ID=k;
以上批次更新方式有兩個缺點:
佔用大量記憶體,必須把1萬個Customer對象先載入到記憶體,然後一一更新它們。
執行的update語句的數目太多,每個update語句只能更新一個Customer對象,必須通過1萬條update語句才能更新1萬個Customer對象,頻繁地訪問資料庫,會大大降低應用的效能。
一般說來,應該儘可能避免在應用程式層進行大量操作,而應該在資料庫層直接進行大量操作,例如直接在資料庫中執行用於批次更新或刪除的SQL語句,如果大量操作的邏輯比較複雜,則可以通過直接在資料庫中啟動並執行預存程序來完成大量操作。
並不是所有的資料庫系統都支援預存程序。例如目前的MySQL就不支援預存程序,因此不能通過預存程序來進行批次更新或大量刪除。
當然,在應用程式層也可以進行大量操作,主要有以下方式:
(1)通過Session來進行大量操作。
(2)通過StatelessSession來進行大量操作。
(3)通過HQL來進行大量操作。
(4)直接通過JDBC API來進行大量操作。
9.4.1 通過Session來進行大量操作
Session的save()以及update()方法都會把處理的對象存放在自己的緩衝中。如果通過一個Session對象來處理大量持久化對象,應該及時從緩衝中清空已經處理完畢並且不會再訪問的對象。具體的做法是在處理完一個對象或小批量對象後,立刻調用flush()方法清理緩衝,然後再調用clear()方法清空緩衝。
通過Session來進行大量操作會受到以下約束:
(1)需要在Hibernate的設定檔中設定JDBC單次批量處理的數目,合理的取值通常為10到50之間,例如:
hibernate.jdbc.batch_size=20
在按照本節介紹的方法進行大量操作時,應該保證每次向資料庫發送的批量SQL語句數目與這個batch_size屬性一致。
(2)如果對象採用"identity"標識符產生器,則Hibernate無法在JDBC層進行批量插入操作。
(3)進行大量操作時,建議關閉Hibernate的第二級緩衝。本書的姊妹篇《精通Hibernate:進階篇》對第二級緩衝做了詳細介紹。Session的緩衝為Hibernate的第一級緩衝,通常它是事務範圍內的緩衝,也就是說,每個事務都有單獨的第一級緩衝。SessionFactory的外置緩衝為Hibernate的第二級緩衝,它是應用範圍內的緩衝,也就是說,所有事務都共用同一個第二級緩衝。在任何情況下,Hibernate的第一級緩衝總是可用的。而預設情況下,Hibernate的第二級緩衝是關閉的,此外也可以在Hibernate的設定檔中通過如下方式顯式關閉第二級緩衝:
hibernate.cache.use_second_level_cache=false
1.批量插入資料
以下代碼一共向資料庫中插入十萬條CUSTOMERS記錄,單次批量插入20條CUSTOMERS記錄:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
for ( int i=0; i<100000; i++ ) {
Customer customer = new Customer(.....);
session.save(customer);
if ( i % 20 == 0 ) { //單次大量操作的數目為20
session.flush(); //清理緩衝,執行批量插入20條記錄的SQL insert語句
session.clear(); //清空緩衝中的Customer對象
}
}
tx.commit();
session.close();
在以上程式中,每次執行session.flush()方法,就會向資料庫中批量插入20條記錄。接下來session.clear()方法把20個剛儲存的Customer對象從緩衝中清空。
為了保證以上程式順利運行,需要遵守以下約束。
在Hibernate的設定檔中,應該把hibernate.jdbc.batch_size屬性也設為20。
關閉第二級緩衝。因為假如使用了第二級緩衝,那麼所有在第一級緩衝(即Session的緩衝)中建立的Customer對象還要先複製到第二級緩衝中,然後再儲存到資料庫中,這會導致大量不必要的開銷。
Customer對象的標識符產生器不能為"identity"。
2.批次更新資料
進行批次更新時,如果一下子把所有對象到載入到Session的緩衝中,然後再在緩衝中一一更新它們,顯然是不可取的。為瞭解決這一問題,可以使用可滾動的結果集org.hibernate.ScrollableResults,Query的scroll()方法返回一個ScrollableResults對象。以下代碼示範批次更新Customer對象,該代碼一開始利用ScrollableResults對象來載入所有的Customer對象:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
ScrollableResults customers= session.createQuery("from Customer")
.scroll(ScrollMode.FORWARD_ONLY);
int count=0;
while ( customers.next() ) {
Customer customer = (Customer) customers.get(0);
customer.setAge(customer.getAge()+1); //更新Customer對象的age屬性
if ( ++count % 20 == 0 ) { //單次大量操作的數目為20
session.flush();//清理緩衝,執行批次更新20條記錄的SQL update語句
session.clear();//清空緩衝中的Customer對象
}
}
tx.commit();
session.close();
在以上代碼中,Query的scroll()方法返回的ScrollableResults對象中實際上並不包含任何Customer對象,它僅僅包含了用於線上定位元據庫中CUSTOMERS記錄的遊標。只有當程式遍曆訪問ScrollableResults對象中的特定元素時,它才會到資料庫中載入相應的Customer對象。
為了保證以上程式順利運行,需要遵守以下約束:
在Hibernate的設定檔中,應該把hibernate.jdbc.batch_size屬性也設為20。
關閉第二級緩衝。假如已經在設定檔中啟用了第二級緩衝,也可以通過以下方式在程式中忽略第二級緩衝:
ScrollableResults customers= session.createQuery("from Customer")
//忽略第二級緩衝
.setCacheMode(CacheMode.IGNORE)
.scroll(ScrollMode.FORWARD_ONLY);
9.4.2 通過StatelessSession來進行大量操作
Session具有一個用於保持記憶體中對象與資料庫中相應資料保持同步的緩衝,位於Session緩衝中的對象為持久化對象。但在進行大量操作時,把大量對象存放在Session緩衝中會消耗大量記憶體空間。作為一種替代方案,可以採用無狀態的StatelessSession來進行大量操作。
以下代碼利用 StatelessSession來進行批次更新操作:
StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
ScrollableResults customers = session.getNamedQuery("GetCustomers")
.scroll(ScrollMode.FORWARD_ONLY);
while ( customers.next() ) {
Customer customer = (Customer) customers.get(0);
customer.setAge(customer.getAge()+1); //在記憶體中更新Customer對象的age屬性;
session.update(customer);//立即執行update語句,更新資料庫中相應CUSTOMERS記錄
}
tx.commit();
session.close();
從形式上看,StatelessSession與Session的用法有點相似。StatelessSession與Session相比,有以下區別:
(1)StatelessSession沒有緩衝,通過StatelessSession來載入、儲存或更新後的對象都處於游離狀態。
(2)StatelessSession不會與Hibernate的第二級緩衝互動。
(3)當調用StatelessSession的save()、update()或delete()方法時,這些方法會立即執行相應的SQL語句,而不會僅僅計劃執行一條SQL語句。
(4)StatelessSession不會對所載入的對象自動進行髒檢查。所以在以上程式中,修改了記憶體中Customer對象的屬性後,還需要通過StatelessSession的update()方法來更新資料庫中的相應資料。
(5)StatelessSession不會對關聯的對象進行任何級聯操作。舉例來說,通過StatelessSession來儲存一個Customer對象時,不會級聯儲存與之關聯的Order對象。
(6)StatelessSession所做的操作可以被Interceptor攔截器捕獲到,但會被Hibernate的事件處理系統忽略。
(7)通過同一個StatelessSession對象兩次載入OID為1的Customer對象時,會得到兩個具有不同記憶體位址的Customer對象,例如:
StatelessSession session = sessionFactory.openStatelessSession();
Customer c1=(Customer)session.get(Customer.class,new Long(1));
Customer c2=(Customer)session.get(Customer.class,new Long(1));
System.out.println(c1==c2); //列印false
9.4.3 通過HQL來進行大量操作
Hibernate3中的HQL(Hibernate Query Language,Hibernate查詢語言)不僅可以檢索資料,還可以用於進行批次更新、刪除和插入資料。大量操作實際上直接在資料庫中完成,所處理的資料不會被儲存在Session的緩衝中,因此不會佔用記憶體空間。
Query.executeUpdate()方法和JDBC API中的PreparedStatement.executeUpdate()很相似,前者執行用於更新、刪除和插入的HQL語句,而後者執行用於更新、刪除和插入的SQL語句。
1.批次更新資料
以下程式碼示範通過HQL來批次更新Customer對象:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
String hqlUpdate =
"update Customer c set c.name = :newName where c.name = :oldName";
int updatedEntities = session.createQuery( hqlUpdate )
.setString( "newName", "Mike" )
.setString( "oldName", "Tom" )
.executeUpdate();
tx.commit();
session.close();
以上程式碼向資料庫發送的SQL語句為:
update CUSTOMERS set NAME="Mike" where NAME="Tom"
2.大量刪除資料
Session的delete()方法一次只能刪除一個對象,不適合進行大量刪除操作。以下程式碼示範通過HQL來大量刪除Customer對象:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
String hqlDelete = "delete Customer c where c.name = :oldName";
int deletedEntities = session.createQuery( hqlDelete )
.setString( "oldName", "Tom" )
.executeUpdate();
tx.commit();
session.close();
以上程式碼向資料庫提交的SQL語句為:
delete from CUSTOMERS where NAME="Tom"
3.批量插入資料
插入資料的HQL文法為:
insert into EntityName properties_list select_statement
以上EntityName表示持久化類的名字,properties_list表示持久化類的屬性列表,select_statement表示子查詢語句。
HQL只支援insert into ... select ... 形式的插入語句,而不支援"insert into ... values ... "形式的插入語句。
下面舉例說明如何通過HQL來批量插入資料。假定有DelinquentAccount和Customer類,它們都有id和name屬性,與這兩個類對應的表分別為DELINQUENT_ACCOUNTS和CUSTOMERS表。DelinquentAccount.hbm.xml和Customer.hbm.xml檔案分別為這兩個類的對應檔。以下代碼能夠把CUSTOMERS表中的資料複製到DELINQUENT_ACCOUNTS表中:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
String hqlInsert = "insert into DelinquentAccount (id, name) select c.id, c.name from Customer c where c.id>1";
int createdEntities = s.createQuery( hqlInsert )
.executeUpdate();
tx.commit();
session.close();
以上程式碼向資料庫提交的SQL語句為:
insert into DELINQUENT_ACCOUNTS(ID,NAME) select ID,NAME from CUSTOMERS where ID>1
9.4.4 直接通過JDBC API來進行大量操作
當通過JDBC API來執行SQL insert、update和delete語句時,SQL語句中涉及到的資料不會被載入到記憶體中,因此不會佔用記憶體空間。
以下程式直接通過JDBC API來執行用於批次更新的SQL語句:
Transaction tx = session.beginTransaction();
//獲得該Session使用的資料庫連接
java.sql.Connection con=session.connection();
//通過JDBC API執行用於批次更新的SQL語句
PreparedStatement stmt=con.prepareStatement("update CUSTOMERS set AGE=AGE+1 "
+"where AGE>0 ");
stmt.executeUpdate();
tx.commit();
以上程式通過Session的connection()方法獲得該Session使用的資料庫連接,然後通過它建立PreparedStatement對象並執行SQL語句。值得注意的是,應用程式仍然通過Hibernate的Transaction介面來聲明事務邊界。
值得注意的是,在Hibernate3中,儘管Session的connection()方法還存在,但是已經被廢棄,不提倡使用了,不過Hibernate3提供了替代方案:org.hibernate.jdbc.Work介面表示直接通過JDBC API來訪問資料庫的操作,Work介面的execute()方法用於執行直接通過JDBC API來訪問資料庫的操作:
public interface Work {
//直接通過JDBC API來訪問資料庫的操作
public void execute(Connection connection) throws SQLException;
}
Session的doWork(Work work)方法用於執行Work對象指定的操作,即調用Work對象的execute()方法。Session會把當前使用的資料庫連接傳給execute()方法。
以下程式示範了通過Work介面以及Session的doWork()方法來執行大量操作的過程:
Transaction tx=session.beginTransaction();
//定義一個匿名類,實現了Work介面
Work work=new Work(){
public void execute(Connection connection)throws SQLException{
//通過JDBC API執行用於批次更新的SQL語句
PreparedStatement stmt=connection
.prepareStatement("update CUSTOMERS set AGE=AGE+1 "
+"where AGE>0 ");
stmt.executeUpdate();
}
};
//執行work
session.doWork(work);
tx.commit();
當通過JDBC API中的PreparedStatement介面來執行SQL語句時,SQL語句中涉及到的資料不會被載入到Session的緩衝中,因此不會佔用記憶體空間。