Hibernate
我們首先介紹在Spring環境中如何使用Hibernate,以此來展示Spring整合ORM對映器的方法。本節詳細介紹了許多問題,並展示了DAO實現和事務劃分的不同變體。這些模式中的大多數可以直接應用於所有其他受支援的ORM工具。本章後面的部分將介紹其他ORM技術並展示簡要示例。
|
從Spring Framework 7.0開始,Spring要求使用Hibernate ORM 7.x 作為 Spring 的
|
Spring 容器中的 SessionFactory 設定
為了避免將應用程式物件與硬編碼的資源查詢繫結,您可以在Spring容器中將資源(例如JDBC DataSource 或 Hibernate SessionFactory)定義為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://:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="mySessionFactory" class="org.springframework.orm.jpa.hibernate.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>
從本地 Jakarta Commons DBCP BasicDataSource 切換到 JNDI 定位 DataSource(通常由應用伺服器管理)僅需更改配置,如下例所示
<beans>
<jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>
您還可以使用 Spring 的 JndiObjectFactoryBean / <jee:jndi-lookup> 訪問 JNDI 定位的 SessionFactory,以檢索並公開它。然而,這在 EJB 上下文之外通常不常見。
|
Spring 還提供了一個
這種原生的 Hibernate 設定還可以公開一個 JPA |
基於純 Hibernate API 實現 DAO
Hibernate 有一個名為上下文會話的功能,其中 Hibernate 本身在每個事務中管理一個當前 Session。這大致相當於 Spring 對每個事務一個 Hibernate Session 的同步。相應的 DAO 實現類似於以下基於純 Hibernate API 的示例
-
Java
-
Kotlin
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();
}
}
class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao {
fun loadProductsByCategory(category: String): Collection<*> {
return sessionFactory.currentSession
.createQuery("from test.Product product where product.category=?")
.setParameter(0, category)
.list()
}
}
這種風格類似於 Hibernate 參考文件和示例中的風格,只是在例項變數中儲存了 SessionFactory。我們強烈建議使用這種基於例項的設定,而不是 Hibernate 的 CaveatEmptor 示例應用程式中的老式 static HibernateUtil 類。(通常,除非絕對必要,否則不要將任何資源儲存在 static 變數中。)
前面的 DAO 示例遵循依賴注入模式。它非常適合 Spring IoC 容器,就像針對 Spring 的 HibernateTemplate 編碼一樣。您也可以在純 Java 中設定這樣的 DAO(例如,在單元測試中)。為此,例項化它並使用所需的工廠引用呼叫 setSessionFactory(..)。作為 Spring bean 定義,DAO 將類似於以下內容
<beans>
<bean id="myProductDao" class="product.ProductDaoImpl">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
</beans>
這種 DAO 風格的主要優點是它僅依賴於 Hibernate API。無需匯入任何 Spring 類。從非侵入性的角度來看,這很有吸引力,並且可能讓 Hibernate 開發人員感到更自然。
然而,DAO 丟擲的是純 HibernateException(它是一個未經檢查的異常,因此無需宣告或捕獲),這意味著呼叫者只能將異常視為一般性致命錯誤——除非他們想依賴 Hibernate 自己的異常層次結構。如果不將呼叫者繫結到實現策略,就無法捕獲特定原因(例如樂觀鎖失敗)。對於強烈基於 Hibernate、不需要任何特殊異常處理或兩者兼而有之的應用程式來說,這種權衡可能是可以接受的。
幸運的是,Spring 的 LocalSessionFactoryBean 支援 Hibernate 的 SessionFactory.getCurrentSession() 方法,適用於任何 Spring 事務策略,即使使用 HibernateTransactionManager 也會返回當前由 Spring 管理的事務 Session。該方法的標準行為仍然是返回與正在進行的 JTA 事務(如果有)關聯的當前 Session。無論您使用 Spring 的 JtaTransactionManager、EJB 容器管理事務(CMT)還是 JTA,此行為都適用。
總而言之,您可以基於純 Hibernate API 實現 DAO,同時仍然能夠參與 Spring 管理的事務。
宣告式事務劃分
我們建議您使用 Spring 的宣告式事務支援,它允許您用 AOP 事務攔截器替換 Java 程式碼中顯式的事務劃分 API 呼叫。您可以透過使用 Java 註解或 XML 在 Spring 容器中配置此事務攔截器。這種宣告式事務功能讓您的業務服務擺脫重複的事務劃分程式碼,並專注於新增業務邏輯,這才是您應用程式的真正價值。
| 在繼續之前,如果您尚未閱讀宣告式事務管理,我們強烈建議您閱讀。 |
您可以使用 @Transactional 註解對服務層進行註解,並指示 Spring 容器查詢這些註解併為這些註解方法提供事務語義。以下示例展示瞭如何做到這一點
-
Java
-
Kotlin
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();
}
}
class ProductServiceImpl(private val productDao: ProductDao) : ProductService {
@Transactional
fun increasePriceOfAllProductsInCategory(category: String) {
val productsToChange = productDao.loadProductsByCategory(category)
// ...
}
@Transactional(readOnly = true)
fun findAllProducts() = productDao.findAllProducts()
}
在容器中,您需要設定 PlatformTransactionManager 實現(作為 bean)和 <tx:annotation-driven/> 條目,在執行時選擇 @Transactional 處理。以下示例展示瞭如何做到這一點
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- SessionFactory, DataSource, etc. omitted -->
<bean id="transactionManager"
class="org.springframework.orm.jpa.hibernate.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<tx:annotation-driven/>
<bean id="myProductService" class="product.SimpleProductService">
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
程式設計式事務劃分
您可以在應用程式的更高層次上劃分事務,在覆蓋任意數量操作的低層次資料訪問服務之上。對周圍業務服務的實現也沒有限制。它只需要一個 Spring PlatformTransactionManager。同樣,後者可以來自任何地方,但最好作為透過 setTransactionManager(..) 方法的 bean 引用。此外,productDAO 應該由 setProductDao(..) 方法設定。以下兩段程式碼片段顯示了 Spring 應用程式上下文中的事務管理器和業務服務定義,以及業務方法實現的一個示例
<beans>
<bean id="myTxManager" class="org.springframework.orm.jpa.hibernate.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>
-
Java
-
Kotlin
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...
}
});
}
}
class ProductServiceImpl(transactionManager: PlatformTransactionManager,
private val productDao: ProductDao) : ProductService {
private val transactionTemplate = TransactionTemplate(transactionManager)
fun increasePriceOfAllProductsInCategory(category: String) {
transactionTemplate.execute {
val productsToChange = productDao.loadProductsByCategory(category)
// do the price increase...
}
}
}
Spring 的 TransactionInterceptor 允許回撥程式碼丟擲任何已檢查的應用程式異常,而 TransactionTemplate 僅限於回撥中的未檢查異常。如果發生未檢查的應用程式異常,或者事務被應用程式標記為僅回滾(透過設定 TransactionStatus),則 TransactionTemplate 會觸發回滾。預設情況下,TransactionInterceptor 的行為相同,但允許為每個方法配置回滾策略。
事務管理策略
TransactionTemplate 和 TransactionInterceptor 都將實際的事務處理委託給 PlatformTransactionManager 例項(它可以是 HibernateTransactionManager (對於單個 Hibernate SessionFactory),在底層使用 ThreadLocal Session)或 JtaTransactionManager (對於 Hibernate 應用程式,委託給容器的 JTA 子系統)。您甚至可以使用自定義的 PlatformTransactionManager 實現。從原生 Hibernate 事務管理切換到 JTA(例如,當您的應用程式的某些部署面臨分散式事務要求時)只是配置問題。您可以將 Hibernate 事務管理器替換為 Spring 的 JTA 事務實現。事務劃分和資料訪問程式碼都可以無需更改地工作,因為它們使用通用的事務管理 API。
對於跨多個 Hibernate 會話工廠的分散式事務,您可以將 JtaTransactionManager 作為事務策略與多個 LocalSessionFactoryBean 定義結合使用。然後,每個 DAO 都會將其特定的 SessionFactory 引用傳遞到其相應的 bean 屬性中。如果所有底層 JDBC 資料來源都是事務性容器資料來源,那麼業務服務可以在任意數量的 DAO 和任意數量的會話工廠之間劃分事務,而無需特殊考慮,只要它使用 JtaTransactionManager 作為策略。
HibernateTransactionManager 和 JtaTransactionManager 都允許對 Hibernate 進行適當的 JVM 級別快取處理,而無需容器特定的事務管理器查詢或 JCA 聯結器(如果您不使用 EJB 啟動事務)。
HibernateTransactionManager 可以將 Hibernate JDBC Connection 匯出到針對特定 DataSource 的純 JDBC 訪問程式碼。這種能力允許在不使用 JTA 的情況下,透過混合 Hibernate 和 JDBC 資料訪問進行高階事務劃分,前提是您只訪問一個數據庫。如果您透過 LocalSessionFactoryBean 類的 dataSource 屬性使用 DataSource 設定傳入的 SessionFactory,則 HibernateTransactionManager 會自動將 Hibernate 事務公開為 JDBC 事務。或者,您可以透過 HibernateTransactionManager 類的 dataSource 屬性明確指定要公開事務的 DataSource。
對於 JTA 風格的實際資源連線的延遲檢索,Spring 為目標連線池提供了一個相應的 DataSource 代理類:請參閱 LazyConnectionDataSourceProxy。這對於 Hibernate 只讀事務特別有用,因為它們通常可以從本地快取處理,而無需訪問資料庫。
比較容器管理和本地定義資源
您可以在容器管理的 JNDI SessionFactory 和本地定義的 SessionFactory 之間切換,而無需更改一行應用程式程式碼。將資源定義保留在容器中還是本地在應用程式中,主要取決於您使用的事務策略。與 Spring 定義的本地 SessionFactory 相比,手動註冊的 JNDI SessionFactory 沒有提供任何好處。透過 Hibernate 的 JCA 聯結器部署 SessionFactory 提供了參與 Jakarta EE 伺服器管理基礎結構的附加價值,但除此之外沒有增加實際價值。
Spring 的事務支援不與容器繫結。當配置為除 JTA 之外的任何策略時,事務支援在獨立或測試環境中也有效。特別是在單資料庫事務的典型情況下,Spring 的單資源本地事務支援是 JTA 的輕量級且強大的替代方案。當您使用本地 EJB 無狀態會話 bean 來驅動事務時,您既依賴 EJB 容器又依賴 JTA,即使您只訪問單個數據庫並僅使用無狀態會話 bean 透過容器管理事務提供宣告式事務。以程式設計方式直接使用 JTA 也需要 Jakarta EE 環境。
Spring 驅動的事務可以與本地定義的 Hibernate SessionFactory 很好地協同工作,就像它們與本地 JDBC DataSource 協同工作一樣,前提是它們訪問單個數據庫。因此,只有當您有分散式事務要求時,才需要使用 Spring 的 JTA 事務策略。JCA 聯結器需要容器特定的部署步驟,並且(顯然)首先需要 JCA 支援。此配置比部署具有本地資源定義和 Spring 驅動事務的簡單 Web 應用程式需要更多工作。
綜上所述,如果您不使用 EJB,請堅持使用本地 SessionFactory 設定和 Spring 的 HibernateTransactionManager 或 JtaTransactionManager。您可以獲得所有好處,包括適當的事務性 JVM 級別快取和分散式事務,而沒有容器部署的不便。透過 JCA 聯結器進行 Hibernate SessionFactory 的 JNDI 註冊僅在與 EJB 結合使用時才具有價值。
Hibernate 造成的虛假應用伺服器警告
在一些 JTA 環境中,如果 XADataSource 實現非常嚴格(目前是某些 WebLogic Server 和 WebSphere 版本),並且 Hibernate 的配置沒有考慮到該環境的 JTA 事務管理器,那麼應用伺服器日誌中可能會出現虛假警告或異常。這些警告或異常表明正在訪問的連線不再有效,或者 JDBC 訪問不再有效,這可能是因為事務不再活動。例如,這是來自 WebLogic 的實際異常
java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No further JDBC access is allowed within this transaction.
另一個常見問題是 JTA 事務後的連線洩漏,Hibernate 會話(以及潛在的底層 JDBC 連線)未正確關閉。
您可以透過讓 Hibernate 瞭解 JTA 事務管理器來解決此類問題,Hibernate 會與 Spring 同步(以及 Spring)。您有兩種選擇
-
將您的 Spring
JtaTransactionManagerbean 傳遞給您的 Hibernate 設定。最簡單的方法是作為LocalSessionFactoryBeanbean 的jtaTransactionManager屬性的 bean 引用(參見Hibernate 事務設定)。Spring 隨後將相應的 JTA 策略提供給 Hibernate。 -
您也可以在
LocalSessionFactoryBean上的 "hibernateProperties" 中明確配置 Hibernate 的 JTA 相關屬性,特別是 "hibernate.transaction.coordinator_class"、"hibernate.connection.handling_mode" 和可能存在的 "hibernate.transaction.jta.platform"(有關這些屬性的詳細資訊,請參閱 Hibernate 手冊)。
本節的其餘部分描述了 Hibernate 瞭解和不瞭解 JTA PlatformTransactionManager 時事件的順序。
當 Hibernate 未配置任何 JTA 事務管理器感知時,JTA 事務提交時會發生以下事件
-
JTA 事務提交。
-
Spring 的
JtaTransactionManager與 JTA 事務同步,因此 JTA 事務管理器透過afterCompletion回撥來呼叫它。 -
除其他活動外,此同步可以透過 Hibernate 的
afterTransactionCompletion回撥(用於清除 Hibernate 快取)觸發 Spring 對 Hibernate 的回撥,然後顯式呼叫 Hibernate 會話上的close(),這會導致 Hibernate 嘗試close()JDBC 連線。 -
在某些環境中,此
Connection.close()呼叫會觸發警告或錯誤,因為應用伺服器不再認為Connection可用,因為事務已提交。
當 Hibernate 配置為感知 JTA 事務管理器時,JTA 事務提交時會發生以下事件
-
JTA 事務已準備好提交。
-
Spring 的
JtaTransactionManager與 JTA 事務同步,因此事務透過 JTA 事務管理器的beforeCompletion回撥被呼叫。 -
Spring 意識到 Hibernate 本身已與 JTA 事務同步,因此其行為與前一種情況不同。特別是,它與 Hibernate 的事務資源管理保持一致。
-
JTA 事務提交。
-
Hibernate 與 JTA 事務同步,因此事務透過 JTA 事務管理器呼叫
afterCompletion回撥,並可以正確清除其快取。