《Spring 5 官方文件》16.ORM和資料訪問
16.3 Hibernate
我們將首先介紹Spring環境中的Hibernate 5,然後通過使用Hibernate 5來演示Spring整合O-R對映器的方法。本節將詳細介紹許多問題,並顯示DAO實現和事務劃分的不同變體。這些模式中大多數可以直接轉換為所有其他支援的ORM工具。本章中的以下部分將通過簡單的例子來介紹其他ORM技術。
從Spring 5.0開始,Spring需要Hibernate ORM對JPA的支援要基於4.3或更高的版本,甚至Hibernate ORM 5.0+可以針對本機Hibernate Session API進行程式設計。請注意,Hibernate團隊可能不會在5.0之前維護任何版本,僅僅專注於5.2以後的版本。
16.3.1在Spring容器中配置SessionFactory
開發者可以將資源如JDBCDataSource
或HibernateSessionFactory
定義為Spring容器中的bean來避免將應用程式物件繫結到硬編碼的資源查詢上。應用物件需要訪問資源的時候,都通過對應的Bean例項進行間接查詢,詳情可以通過下一節的DAO的定義來參考。
下面引用的應用的XML元資料定義就展示瞭如何配置JDBC的DataSource
和Hibernate
的SessionFactory
的:
<beans> <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/> <property name="username" value="sa"/> <property name="password" value=""/> </bean> <bean id="mySessionFactory"class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> <property name="dataSource" ref="myDataSource"/> <property name="mappingResources"> <list> <value>product.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <value> hibernate.dialect=org.hibernate.dialect.HSQLDialect </value> </property> </bean> </beans>
這樣,從本地的Jaksrta Commons DBCP的BasicDataSource
轉換到JNDI定位的DataSource
僅僅只需要修改配置檔案。
<beans> <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/> </beans>
開發者也可以通過Spring的JndiObjectFactoryBean
或者<jee:jndi-lookup>
來獲取對應Bean以訪問JNDI定位的SessionFactory
。但是,JNDI定位的SessionFactory
在EJB上下文不常見。
16.3.2基於Hibernate API來實現DAO
Hibernate有一個特性稱之為上下文會話,在每個Hibernate本身每個事務都管理一個當前的Session
。這大致相當於Spring每個事務的一個HibernateSession
的同步。如下的DAO的實現類就是基於簡單的Hibernate API實現的:
public class ProductDaoImpl implements ProductDao { private SessionFactory sessionFactory; public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public Collection loadProductsByCategory(String category) { return this.sessionFactory.getCurrentSession() .createQuery("from test.Product product where product.category=?") .setParameter(0, category) .list(); } }
除了需要在例項中持有SessionFactory
引用以外,上面的程式碼風格跟Hibernate文件中的例子十分相近。Spring團隊強烈建議使用這種基於例項變數的實現風格,而非守舊的static HibernateUtil
風格(總的來說,除非絕對必要,否則儘量不要使用static
變數來持有資源)。
上面DAO的實現完全符合Spring依賴注入的樣式:這種方式可以很好的整合Spring IoC容器,就好像Spring的HibernateTemplate
程式碼一樣。當然,DAO層的實現也可以通過純Java的方式來配置(比如在UT中)。簡單例項化ProductDaoImpl
並且呼叫setSessionFactory(...)
即可。當然,也可以使用Spring bean來進行注入,參考如下XML配置:
<beans> <bean id="myProductDao" class="product.ProductDaoImpl"> <property name="sessionFactory" ref="mySessionFactory"/> </bean> </beans>
上面的DAO實現方式的好處在於只依賴於Hibernate API,而無需引入Spring的class。這從非侵入性的角度來看當然是有吸引力的,毫無疑問,這種開發方式會令Hibernate開發人員將會更加自然。
然而,DAO層會丟擲Hibernate自有異常HibernateException
(屬於非檢查異常,無需顯式宣告和使用try-catch),但是也意味著呼叫方會將異常看做致命異常——除非呼叫方將Hibernate異常體系作為應用的異常體系來處理。而在這種情況下,除非呼叫方自己來實現一定的策略,否則捕獲一些諸如樂觀鎖失敗之類的特定錯誤是不可能的。對於強烈基於Hibernate的應用程式或不需要對特殊異常處理的應用程式,這種代價可能是可以接受的。
幸運的是,Spring的LocalSessionFactoryBean
可以通過Hibernate的SessionFactory.getCurrentSession()
方法為所有的Spring事務策略提供支援,使用HibernateTransactionManager
返回當前的Spring管理的事務的Session
。當然,該方法的標準行為仍然是返回與正在進行的JTA事務相關聯的當前Session
(如果有的話)。無論開發者是使用Spring的JtaTransactionManager
,EJB容器管理事務(CMT)還是JTA,都會適用此行為。
總而言之:開發者可以基於純Hibernate API來實現DAO,同時也可以整合Spring來管理事務。
16.3.3宣告式事務劃分
Spring團隊建議開發者使用Spring宣告式的事務支援,這樣可以通過AOP事務攔截器來替代事務API的顯式呼叫。AOP事務攔截器可以在Spring容器中使用XML或者Java的註解來進行配置。這種事務攔截器可以令開發者的程式碼和重複的事務程式碼相解耦,而開發者可以將精力更多集中在業務邏輯上,而業務邏輯才是應用的核心。
在繼續之前,強烈建議開發者先查閱章節13.5 宣告式事務管理的內容。
開發者可以在服務層的程式碼使用註解@Transactional
,這樣可以讓Spring容器找到這些註解,以對其中註解了的方法提供事務語義。
public class ProductServiceImpl implements ProductService { private ProductDao productDao; public void setProductDao(ProductDao productDao) { this.productDao = productDao; } @Transactional public void increasePriceOfAllProductsInCategory(final String category) { List productsToChange = this.productDao.loadProductsByCategory(category); // ... } @Transactional(readOnly = true) public List<Product> findAllProducts() { return this.productDao.findAllProducts(); } }
開發者所需要做的就是在容器中配置PlatformTransactionManager
的實現,或者是在XML中配置<tx:annotation-driver/>
標籤,這樣就可以在執行時支援@Transactional
的處理了。參考如下XML程式碼:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- SessionFactory, DataSource, etc. omitted --> <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> <tx:annotation-driven/> <bean id="myProductService" class="product.SimpleProductService"> <property name="productDao" ref="myProductDao"/> </bean> </beans>
16.3.4程式設計式事務劃分
開發者可以在應用程式的更高級別上對事務進行標定,而不用考慮低級別的資料訪問執行了多少操作。這樣不會對業務服務的實現進行限制;只需要定義一個Spring的PlatformTransactionManager
即可。當然,PlatformTransactionManager
可以從多處獲取,但最好是通過setTransactionManager(..)
方法以Bean來注入,正如ProductDAO
應該由setProductDao(..)
方法配置一樣。下面的程式碼顯示Spring應用程式上下文中的事務管理器和業務服務的定義,以及業務方法實現的示例:
<beans> <bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> <property name="sessionFactory" ref="mySessionFactory"/> </bean> <bean id="myProductService" class="product.ProductServiceImpl"> <property name="transactionManager" ref="myTxManager"/> <property name="productDao" ref="myProductDao"/> </bean> </beans>
public class ProductServiceImpl implements ProductService { private TransactionTemplate transactionTemplate; private ProductDao productDao; public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionTemplate = new TransactionTemplate(transactionManager); } public void setProductDao(ProductDao productDao) { this.productDao = productDao; } public void increasePriceOfAllProductsInCategory(final String category) { this.transactionTemplate.execute(new TransactionCallbackWithoutResult() { public void doInTransactionWithoutResult(TransactionStatus status) { List productsToChange = this.productDao.loadProductsByCategory(category); // do the price increase... } }); } }
Spring的TransactionInterceptor
允許任何檢查的應用異常到callback
程式碼中去,而TransactionTemplate
還會非受檢異常觸發進行回撥。TransactionTemplate
則會因為非受檢異常或者是由應用標記事務回滾(通過TransactionStatus
)。TransactionInterceptor
也是一樣的處理邏輯,但是同時還允許基於方法配置回滾策略。
16.3.5事務管理策略
無論是TransactionTemplate
或者是TransactionInterceptor
都將實際的事務處理代理到PlatformTransactionManager
例項上來進行處理的,這個例項的實現可以是一個HibernateTransactionManager
(包含一個Hibernate的SessionFactory
通過使用ThreadLocal
的Session
),也可以是JatTransactionManager
(代理到容器的JTA子系統)。開發者甚至可以使用一個自定義的PlatformTransactionManager
的實現。現在,如果應用有需求需要需要部署分散式事務的話,只是一個配置變化,就可以從本地Hibernate事務管理切換到JTA。簡單地用Spring的JTA事務實現來替換Hibernate事務管理器即可。因為引用的PlatformTransactionManager
的是通用事務管理API,事務管理器之間的切換是無需修改程式碼的。
對於那些跨越了多個Hibernate會話工廠的分散式事務,只需要將JtaTransactionManager
和多個LocalSessionFactoryBean
定義相結合即可。每個DAO之後會獲取一個特定的SessionFactory
引用。如果所有底層JDBC資料來源都是事務性容器,那麼只要使用JtaTransactionManager
作為策略實現,業務服務就可以劃分任意數量的DAO和任意數量的會話工廠的事務。
無論是HibernateTransactionManager
還是JtaTransactionManager
都允許使用JVM級別的快取來處理Hibernate,無需基於容器的事務管理器查詢,或者JCA聯結器(如果開發者沒有使用EJB來例項化事務的話)。
HibernateTransactionManager
可以為指定的資料來源的Hibernate JDBC的Connection
轉成為純JDBC的訪問程式碼。如果開發者僅訪問一個數據庫,則開發者完全可以不使用JTA,通過Hibernate和JDBC資料訪問進行高級別事務劃分。如果開發者已經通過LocalSessionFactoryBean
的dataSource
屬性與DataSource
設定了傳入的SessionFactory
,HibernateTransactionManager
會自動將Hibernate事務公開為JDBC事務。或者,開發者可以通過HibernateTransactionManager
的dataSource
屬性的配置以確定公開事務的型別。
16.3.6對比由容器管理的和本地定義的資源
開發者可以在不修改一行程式碼的情況下,在容器管理的JNDISessionFactory
和本地定義的SessionFactory
之間進行切換。是否將資源定義保留在容器中,還是僅僅留在應用中,都取決於開發者使用的事務策略。相對於Spring定義的本地SessionFactory
來說,手動註冊的JNDISessionFactory
沒有什麼優勢。通過Hibernate的JCA聯結器來發佈一個SessionFactory
只會令程式碼更符合J2EE服務標準,但是並不會帶來任何實際的價值。
Spring對事務支援不限於容器。使用除JTA之外的任何策略配置,事務都可以在獨立或測試環境中工作。特別是在單資料庫事務的典型情況下,Spring的單一資源本地事務支援是一種輕量級和強大的替代JTA的解決方案。當開發者使用本地EJB無狀態會話Bean來驅動事務時,即使只訪問單個數據庫,並且只使用無狀態會話Bean來通過容器管理的事務來提供宣告式事務,開發者的程式碼依然是依賴於EJB容器和JTA的。同時,以程式設計方式直接使用JTA也需要一個J2EE環境的。 JTA不涉及JTA本身和JNDI DataSource例項方面的容器依賴關係。對於非Spring,JTA驅動的Hibernate事務,開發者必須使用Hibernate JCA聯結器或開發額外的Hibernate事務程式碼,併為JVM級快取正確配置TransactionManagerLookup
。
Spring驅動的事務可以與本地定義的HibernateSessionFactory
一樣工作,就像本地JDBC DataSource訪問單個數據庫一樣。但是,當開發者有分散式事務的要求的情況下,只能選擇使用Spring JTA事務策略。JCA聯結器是需要特定容器遵循一致的部署步驟的,而且顯然JCA支援是需要放在第一位的。JCA的配置需要比部署本地資源定義和Spring驅動事務的簡單web應用程式需要更多額外的的工作。同時,開發者還需要使用容器的企業版,比如,如果開發者使用的是WebLogic Express的非企業版,就是不支援JCA的。具有跨越單個數據庫的本地資源和事務的Spring應用程式適用於任何基於J2EE的Web容器(不包括JTA,JCA或EJB),如Tomcat,Resin或甚至是Jetty。此外,開發者可以輕鬆地在桌面應用程式或測試套件中重用中間層程式碼。
綜合前面的敘述,如果不使用EJB,請儘量使用本地的SessionFactory
設定和Spring的HibernateTransactionManager
或JtaTransactionManager
。開發者能夠得到了前面提到的所有好處,包括適當的事務性JVM級快取和分散式事務支援,而且沒有容器部署的不便。只有必須配合EJB使用的時候,JNDI通過JCA聯結器來註冊HibernateSessionFactory
才有價值。
16.3.7Hibernate的虛假應用伺服器警告
在某些具有非常嚴格的XADataSource
實現的JTA環境(目前只有一些WebLogic Server和WebSphere版本)中,當配置Hibernate時,沒有考慮到JTA的 PlatformTransactionManager
物件,可能會在應用程式伺服器日誌中顯示虛假警告或異常。這些警告或異常經常描述正在訪問的連線不再有效,或者JDBC訪問不再有效。這通常可能是因為事務不再有效。例如,這是WebLogic的一個實際異常:
java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.
開發者可以通過配置令Hibernate意識到Spring中同步的JTAPlatformTransactionManager
例項的存在,這樣即可消除掉前面所說的虛假警告資訊。開發者有以下兩種選擇:
- 如果在應用程式上下文中,開發者已經直接獲取了JTA
PlatformTransactionManager
物件(可能是從JNDI到JndiObjectFactoryBean
或者<jee:jndi-lookup>
標籤),並將其提供給Spring的JtaTransactionManager
(其中最簡單的方法就是指定一個引用bean將此JTAPlatformTransactionManager
例項定義為LocalSessionFactoryBean
的jtaTransactionManager
屬性的值)。 Spring之後會令PlatformTransactionManager
物件對Hibernate可見。 - 更有可能開發者無法獲取JTA
PlatformTransactionManager
例項,因為Spring的JtaTransactionManager
是可以自己找到該例項的。因此,開發者需要配置Hibernate令其直接查詢JTAPlatformTransactionManager
。開發者可以如Hibernate手冊中所述那樣通過在Hibernate配置中配置應用程式伺服器特定的TransactionManagerLookup
類來執行此操作。
本節的其餘部分描述了在PlatformTransactionManager
對Hibernate可見和PlatformTransactionManager
對Hibernate不可見的情況下發生的事件序列:
當Hibernate未配置任何對JTAPlatformTransactionManager
的進行查詢時,JTA事務提交時會發生以下事件:
- JTA事務提交
- Spring的
JtaTransactionManager
與JTA事務同步,所以它被JTA事務管理器通過afterCompletion
回撥呼叫。 - 在其他活動中,此同步令Spring通過Hibernate的
afterTransactionCompletion
觸發回撥(用於清除Hibernate快取),然後在Hibernate Session上呼叫close()
,從而令Hibernate嘗試close()
JDBC連線。 - 在某些環境中,因為事務已經提交,應用程式伺服器會認為
Connection
不可用,導致Connection.close()
呼叫會觸發警告或錯誤。
當Hibernate配置了對JTAPlatformTransactionManager
進行查詢時,JTA事務提交時會發生以下事件:
- JTA事務準備提交
- Spring的
JtaTransactionManager
與JTA事務同步,所以JTA事務管理器通過beforeCompletion
方法來回調事務。 - Spring確定Hibernate與JTA事務同步,並且行為與前一種情況不同。假設Hibernate Session需要關閉,Spring將會關閉它。
- JTA事務提交。
- Hibernate與JTA事務同步,所以JTA事務管理器通過
afterCompletion
方法回撥事務,可以正確清除其快取。