使用 Hibernate 將 100,000 條記錄插入到數據庫的一個很天真的做法可能是這樣的:
<!-- -->Session<!-- --> session <!-- -->=<!-- --> sessionFactory<!-- -->.<!-- -->openSession<!-- -->(); <!-- --> Transaction tx = session.beginTransaction(); <!-- --> for ( int i=0; i<100000; i++ ) { <!-- --> Customer customer = new Customer(.....); <!-- --> session.save(customer); <!-- --> } <!-- --> tx.commit(); <!-- --> session.close();
這段程序大概運行到 50,000 條記錄左右會失敗並拋出內存溢出異常(OutOfMemoryException)
。這是因爲 Hibernate 把所有新插入的客戶(Customer)
實例在 session 級別的緩存區進行了緩存的緣故。
我們會在本章告訴你如何避免此類問題。首先,如果你要執行批量處理並且想要達到一個理想的性能,那麼使用 JDBC 的批量(batching)功能是至關重要。將 JDBC 的批量抓取數量(batch size)參數設置到一個合適值(比如,10 - 50 之間):
hibernate.jdbc.batch_size 20
注意,假若你使用了 identiy
標識符生成器,Hibernate 在 JDBC 級別透明的關閉插入語句的批量執行。 15.1. 批量插入(Batch inserts)15.2. 批量更新(Batch updates)15.3. StatelessSession(無狀態 session)接口15.4. DML(數據操作語言)風格的操作(DML-style operations)HQL)來執行大批量 SQL 風格的 DML 語句的方法。
你也可能想在執行批量處理時完全關閉二級緩存:
hibernate.cache.use_second_level_cache false
但是,這不是絕對必須的,因爲我們可以顯式設置 CacheMode
來關閉與二級緩存的交互。
如果要將很多對象持久化,你必須通過經常的調用 flush()
以及稍後調用 clear()
來控制第一級緩存的大小。
<!-- -->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, same as the JDBC batch size <!-- --> //flush a batch of inserts and release memory: <!-- --> session.flush(); <!-- --> session.clear(); <!-- --> } <!-- --> } <!-- --> <!-- --> tx.commit(); <!-- --> session.close();
此方法同樣適用於檢索和更新數據。此外,在進行會返回很多行數據的查詢時,你需要使用 scroll()
方法以便充分利用服務器端遊標所帶來的好處。
<!-- -->Session<!-- --> session <!-- -->=<!-- --> sessionFactory<!-- -->.<!-- -->openSession<!-- -->(); <!-- --> Transaction tx = session.beginTransaction(); <!-- --> <!-- --> ScrollableResults customers = session.getNamedQuery("GetCustomers") <!-- --> .setCacheMode(CacheMode.IGNORE) <!-- --> .scroll(ScrollMode.FORWARD_ONLY); <!-- --> int count=0; <!-- --> while ( customers.next() ) { <!-- --> Customer customer = (Customer) customers.get(0); <!-- --> customer.updateStuff(...); <!-- --> if ( ++count % 20 == 0 ) { <!-- --> //flush a batch of updates and release memory: <!-- --> session.flush(); <!-- --> session.clear(); <!-- --> } <!-- --> } <!-- --> <!-- --> tx.commit(); <!-- --> session.close();
作爲選擇,Hibernate 提供了基於命令的 API,可以用 detached object 的形式把數據以流的方法加入到數據庫,或從數據庫輸出。StatelessSession
沒有持久化上下文,也不提供多少高層的生命週期語義。特別是,無狀態 session 不實現第一級 cache,也不和第二級緩存,或者查詢緩存交互。它不實現事務化寫,也不實現髒數據檢查。用 stateless session 進行的操作甚至不級聯到關聯實例。stateless session 忽略集合類(Collections)。通過 stateless session 進行的操作不觸發 Hibernate 的事件模型和攔截器。無狀態 session 對數據的混淆現象免疫,因爲它沒有第一級緩存。無狀態 session 是低層的抽象,和低層 JDBC 相當接近。
<!-- -->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.updateStuff(...); <!-- --> session.update(customer); <!-- --> } <!-- --> <!-- --> tx.commit(); <!-- --> session.close();
注意在上面的例子中,查詢返回的 Customer
實例立即被脫管(detach)。它們與任何持久化上下文都沒有關係。
StatelessSession
接口定義的 insert(), update()
和 delete()
操作是直接的數據庫行級別操作,其結果是立刻執行一條 INSERT, UPDATE
或 DELETE
語句。因此,它們的語義和 Session
接口定義的 save(), saveOrUpdate()
和delete()
操作有很大的不同。
就像已經討論的那樣,自動和透明的對象/關係映射(object/relational mapping)關注於管理對象的狀態。這就意味着對象的狀態存在於內存,因此直接操作(使用 SQL Data Manipulation Language
(DML,數據操作語言)語句 :INSERT
,UPDATE
和 DELETE
) 數據庫中的數據將不會影響內存中的對象狀態和對象數據。不過,Hibernate 提供通過 Hibernate 查詢語言(
UPDATE
和 DELETE
語句的僞語法爲:( UPDATE | DELETE ) FROM? EntityName (WHERE where_conditions)?
。
要注意的事項:
-
在 FROM 子句(from-clause)中,FROM 關鍵字是可選的
-
在 FROM 子句(from-clause)中只能有一個實體名,它可以是別名。如果實體名是別名,那麼任何被引用的屬性都必須加上此別名的前綴;如果不是別名,那麼任何有前綴的屬性引用都是非法的。
-
不能在大批量 HQL 語句中使用 joins 連接(顯式或者隱式的都不行)。不過在 WHERE 子句中可以使用子查詢。可以在 where 子句中使用子查詢,子查詢本身可以包含 join。
-
整個 WHERE 子句是可選的。
舉個例子,使用 Query.executeUpdate()
方法執行一個 HQL UPDATE
語句(方法命名是來源於 JDBC 的 PreparedStatement.executeUpdate()
):
<!-- -->Session<!-- --> session <!-- -->=<!-- --> sessionFactory<!-- -->.<!-- -->openSession<!-- -->(); <!-- --> Transaction tx = session.beginTransaction(); <!-- --> String hqlUpdate = "update Customer c set c.name = :newName where c.name = :oldName"; <!-- --> // or String hqlUpdate = "update Customer set name = :newName where name = :oldName"; <!-- --> int updatedEntities = s.createQuery( hqlUpdate ) <!-- --> .setString( "newName", newName ) <!-- --> .setString( "oldName", oldName ) <!-- --> .executeUpdate(); <!-- --> tx.commit(); <!-- --> session.close();
HQL UPDATE
語句,默認不會影響更新實體的 version 或 the timestamp 屬性值。這和 EJB3 規範是一致的。但是,通過使用 versioned update
,你可以強制 Hibernate 正確的重置version
或者 timestamp
屬性值。這通過在 UPDATE
關鍵字後面增加 VERSIONED
關鍵字來實現的。
<!-- -->Session<!-- --> session <!-- -->=<!-- --> sessionFactory<!-- -->.<!-- -->openSession<!-- -->(); <!-- --> Transaction tx = session.beginTransaction(); <!-- --> String hqlVersionedUpdate = "update versioned Customer set name = :newName where name = :oldName"; <!-- --> int updatedEntities = s.createQuery( hqlUpdate ) <!-- --> .setString( "newName", newName ) <!-- --> .setString( "oldName", oldName ) <!-- --> .executeUpdate(); <!-- --> tx.commit(); <!-- --> session.close();
注意,自定義的版本類型(org.hibernate.usertype.UserVersionType
)不允許和 update versioned
語句聯用。
執行一個 HQL DELETE
,同樣使用 Query.executeUpdate()
方法:
<!-- -->Session<!-- --> session <!-- -->=<!-- --> sessionFactory<!-- -->.<!-- -->openSession<!-- -->(); <!-- --> Transaction tx = session.beginTransaction(); <!-- --> String hqlDelete = "delete Customer c where c.name = :oldName"; <!-- --> // or String hqlDelete = "delete Customer where name = :oldName"; <!-- --> int deletedEntities = s.createQuery( hqlDelete ) <!-- --> .setString( "oldName", oldName ) <!-- --> .executeUpdate(); <!-- --> tx.commit(); <!-- --> session.close();
由 Query.executeUpdate()
方法返回的整型
值表明了受此操作影響的記錄數量。注意這個數值可能與數據庫中被(最後一條 SQL 語句)影響了的“行”數有關,也可能沒有。一個大批量 HQL 操作可能導致多條實際的SQL語句被執行,舉個例子,對 joined-subclass 映射方式的類進行的此類操作。這個返回值代表了實際被語句影響了的記錄數量。在那個 joined-subclass 的例子中, 對一個子類的刪除實際上可能不僅僅會刪除子類映射到的表而且會影響“根”表,還有可能影響與之有繼承關係的 joined-subclass 映射方式的子類的表。
INSERT
語句的僞碼是:INSERT INTO EntityName properties_list select_statement
。要注意的是:
-
只支持 INSERT INTO ... SELECT ... 形式,不支持 INSERT INTO ... VALUES ... 形式。
properties_list 和 SQL
INSERT
語句中的字段定義(column speficiation)
類似。對參與繼承樹映射的實體而言,只有直接定義在給定的類級別的屬性才能直接在 properties_list 中使用。超類的屬性不被支持;子類的屬性無意義。換句話說,INSERT
天生不支持多態性。 -
select_statement 可以是任何合法的 HQL 選擇查詢,不過要保證返回類型必須和要插入的類型完全匹配。目前,這一檢查是在查詢編譯的時候進行的,而不是把它交給數據庫。注意,在Hibernate
Type
間如果只是等價(equivalent)而非相等(equal),會導致問題。定義爲org.hibernate.type.DateType
和org.hibernate.type.TimestampType
的兩個屬性可能會產生類型不匹配錯誤,雖然數據庫級可能不加區分或者可以處理這種轉換。 -
對 id 屬性來說,insert 語句給你兩個選擇。你可以明確地在 properties_list 表中指定 id 屬性(這樣它的值是從對應的 select 表達式中獲得),或者在 properties_list 中省略它(此時使用生成指)。後一種選擇只有當使用在數據庫中生成值的 id 產生器時才能使用;如果是“內存”中計算的類型生成器,在解析時會拋出一個異常。注意,爲了說明這一問題,數據庫產生值的生成器是
org.hibernate.id.SequenceGenerator
(和它的子類),以及任何org.hibernate.id.PostInsertIdentifierGenerator
接口的實現。這兒最值得注意的意外是org.hibernate.id.TableHiLoGenerator
,它不能在此使用,因爲它沒有得到其值的途徑。 -
對映射爲
version
或timestamp
的屬性來說,insert 語句也給你兩個選擇,你可以在 properties_list 表中指定(此時其值從對應的 select 表達式中獲得),或者在 properties_list 中省略它(此時,使用在org.hibernate.type.VersionType
中定義的seed value(種子值)
)。
下面是一個執行 HQL INSERT
語句的例子:
<!-- -->Session<!-- --> session <!-- -->=<!-- --> sessionFactory<!-- -->.<!-- -->openSession<!-- -->(); <!-- --> Transaction tx = session.beginTransaction(); <!-- --> String hqlInsert = "insert into DelinquentAccount (id, name) select c.id, c.name from Customer c where ..."; <!-- --> int createdEntities = s.createQuery( hqlInsert ) <!-- --> .executeUpdate(); <!-- --> tx.commit(); <!-- --> session.close();