Hibernate
我們首先介紹 Hibernate 5 在 Spring 環境中的應用,以此演示 Spring 在整合 OR 對映器方面所採取的方法。本節詳細討論了許多問題,並展示了 DAO 實現和事務劃分的不同變體。這些模式中的大多數都可以直接應用於所有其他受支援的 ORM 工具。本章後面的部分將介紹其他 ORM 技術並提供簡短示例。
自 Spring Framework 6.0 起,Spring 要求 Hibernate ORM 5.5+ 用於 Spring 的 Hibernate ORM 6.x 僅作為 JPA 提供程式( |
在 Spring 容器中配置 SessionFactory
為了避免將應用程式物件與硬編碼的資源查詢繫結,您可以將資源(如 JDBC DataSource
或 Hibernate SessionFactory
)定義為 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://: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>
從本地 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 有一個稱為上下文會話(contextual sessions)的特性,其中 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(例如,在單元測試中)。為此,例項化它並使用所需的 factory 引用呼叫 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 容器管理事務(CMTs)還是 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.hibernate5.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.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>
-
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
僅限於回撥中的非受控異常。TransactionTemplate
在發生非受控應用程式異常或事務被應用程式標記為僅回滾時(透過設定 TransactionStatus
)觸發回滾。預設情況下,TransactionInterceptor
表現相同,但允許根據每個方法配置回滾策略。
事務管理策略
TransactionTemplate
和 TransactionInterceptor
都將實際的事務處理委託給 PlatformTransactionManager
例項(對於單個 Hibernate SessionFactory
,可以是使用底層 ThreadLocal
Session
的 HibernateTransactionManager
),或者對於 Hibernate 應用程式,可以是 JtaTransactionManager
(委託給容器的 JTA 子系統)。您甚至可以使用自定義的 PlatformTransactionManager
實現。從原生 Hibernate 事務管理切換到 JTA(例如,當您的應用程式的某些部署面臨分散式事務需求時)僅僅是配置問題。您可以將 Hibernate 事務管理器替換為 Spring 的 JTA 事務實現。事務劃分和資料訪問程式碼無需更改,因為它們使用通用的事務管理 API。
對於跨多個 Hibernate session factory 的分散式事務,您可以將 JtaTransactionManager
作為事務策略與多個 LocalSessionFactoryBean
定義結合使用。然後,每個 DAO 會將其對應的 bean 屬性傳遞一個特定的 SessionFactory
引用。如果所有底層 JDBC 資料來源都是事務性的容器資料來源,則業務服務可以在任意數量的 DAO 和任意數量的 session factory 之間劃分事務,而無需特別考慮,只要它使用 JtaTransactionManager
作為策略即可。
HibernateTransactionManager
和 JtaTransactionManager
都允許使用 Hibernate 進行適當的 JVM 級別快取處理,無需特定於容器的事務管理器查詢或 JCA 聯結器(如果您不使用 EJB 啟動事務)。
HibernateTransactionManager
可以將 Hibernate JDBC Connection
匯出到用於特定 DataSource
的原生 JDBC 訪問程式碼。這種能力允許在混合使用 Hibernate 和 JDBC 資料訪問時進行高級別的事務劃分,完全無需 JTA,前提是您只訪問一個數據庫。如果您已透過 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 來驅動事務時,即使您只訪問單個數據庫並僅使用無狀態會話 Bean 透過容器管理的事務提供宣告性事務,您也依賴於 EJB 容器和 JTA。直接透過程式設計方式使用 JTA 也需要 Jakarta EE 環境。
Spring 驅動的事務可以與本地定義的 Hibernate SessionFactory
一樣好地工作,就像它們與本地 JDBC DataSource
一樣,前提是它們訪問的是單個數據庫。因此,只有在您有分散式事務需求時,才需要使用 Spring 的 JTA 事務策略。JCA 聯結器需要容器特定的部署步驟,並且首先需要 JCA 支援。與使用本地資源定義和 Spring 驅動事務部署一個簡單的 Web 應用程式相比,這種配置需要更多工作。
總而言之,如果您不使用 EJB,請堅持使用本地 SessionFactory
設定和 Spring 的 HibernateTransactionManager
或 JtaTransactionManager
。您可以獲得所有好處,包括適當的事務性 JVM 級別快取和分散式事務,而沒有容器部署的不便。透過 JCA 聯結器註冊 Hibernate SessionFactory
僅在與 EJB 結合使用時才增加價值。
使用 Hibernate 時出現的虛假應用伺服器警告
在某些具有非常嚴格的 XADataSource
實現的 JTA 環境中(目前是某些 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
JtaTransactionManager
bean 傳遞給您的 Hibernate 設定。最簡單的方法是將 bean 引用注入到您的LocalSessionFactoryBean
bean 的jtaTransactionManager
屬性中(請參閱 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
回撥呼叫它。 -
在其他活動中,此同步可以觸發 Spring 透過 Hibernate 的
afterTransactionCompletion
回撥(用於清除 Hibernate 快取)回撥 Hibernate,然後對 Hibernate 會話執行顯式的close()
呼叫,這會導致 Hibernate 嘗試close()
JDBC Connection。 -
在某些環境中,此
Connection.close()
呼叫然後觸發警告或錯誤,因為應用伺服器不再認為該Connection
可用,因為事務已提交。
當配置 Hibernate 時感知 JTA 事務管理器,當 JTA 事務提交時,會發生以下事件:
-
JTA 事務準備提交。
-
Spring 的
JtaTransactionManager
與 JTA 事務同步,因此事務管理器透過beforeCompletion
回撥呼叫事務。 -
Spring 意識到 Hibernate 本身與 JTA 事務同步,並且其行為與前一種情況不同。特別是,它與 Hibernate 的事務性資源管理對齊。
-
JTA 事務提交。
-
Hibernate 與 JTA 事務同步,因此事務管理器透過
afterCompletion
回撥呼叫事務,並且可以正確清除其快取。