Hibernate
我們先從 Spring 環境中 Hibernate 5 的涵蓋範圍開始,使用它來示範 Spring 在整合 OR mapper 時採用的方法。本節詳細介紹了許多問題,並展示了 DAO 實作和交易劃分的不同變化。這些模式中的大多數可以直接轉換為所有其他支援的 ORM 工具。本章後面的章節將介紹其他 ORM 技術,並展示簡短的範例。
從 Spring Framework 6.0 開始,Spring 需要 Hibernate ORM 5.5+ 用於 Spring 的 Hibernate ORM 6.x 僅支援作為 JPA 提供者 ( |
SessionFactory
在 Spring 容器中的設定
為了避免將應用程式物件與硬式編碼的資源查找綁定,您可以將資源 (例如 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://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>
從本機 Jakarta Commons DBCP BasicDataSource
切換到 JNDI 定位的 DataSource
(通常由應用程式伺服器管理) 僅是組態問題,如下列範例所示
<beans>
<jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>
您也可以存取 JNDI 定位的 SessionFactory
,使用 Spring 的 JndiObjectFactoryBean
/ <jee:jndi-lookup>
來檢索和公開它。但是,這通常在 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 (例如,在單元測試中)。為此,實例化它並使用所需的 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 容器管理交易 (CMT) 還是 JTA,此行為都適用。
總之,您可以基於純 Hibernate API 實作 DAO,同時仍然能夠參與 Spring 管理的交易。
宣告式交易劃分
我們建議您使用 Spring 的宣告式交易支援,這可讓您使用 AOP 交易攔截器取代 Java 程式碼中的明確交易劃分 API 呼叫。您可以使用 Java 註解或 XML 在 Spring 容器中組態此交易攔截器。這種宣告式交易功能可讓您的業務服務免於重複的交易劃分程式碼,並專注於新增業務邏輯,這才是您應用程式的真正價值。
在繼續之前,如果您尚未閱讀過 Declarative Transaction Management,我們強烈建議您閱讀。 |
您可以使用 @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
僅限於回呼中的未檢查例外。如果發生未檢查的應用程式例外或交易被應用程式標記為僅回滾 (透過設定 TransactionStatus
),TransactionTemplate
會觸發回滾。預設情況下,TransactionInterceptor
的行為方式相同,但允許每個方法可組態的回滾原則。
交易管理策略
TransactionTemplate
和 TransactionInterceptor
都將實際交易處理委派給 PlatformTransactionManager
實例 (對於 Hibernate 應用程式,可以是 HibernateTransactionManager
(用於單個 Hibernate SessionFactory
),方法是在幕後使用 ThreadLocal Session
) 或 JtaTransactionManager
(委派給容器的 JTA 子系統)。您甚至可以使用自訂 PlatformTransactionManager
實作。從原生 Hibernate 交易管理切換到 JTA (例如,當您的應用程式的某些部署面臨分散式交易需求時) 僅是組態問題。您可以將 Hibernate 交易管理器替換為 Spring 的 JTA 交易實作。交易劃分和資料存取程式碼都無需變更即可運作,因為它們使用泛型交易管理 API。
對於跨多個 Hibernate session factory 的分散式交易,您可以將 JtaTransactionManager
作為交易策略與多個 LocalSessionFactoryBean
定義結合使用。然後,每個 DAO 都會取得一個特定的 SessionFactory
參考傳遞到其對應的 bean 屬性中。如果所有底層 JDBC 資料來源都是交易容器,則業務服務可以跨任意數量的 DAO 和任意數量的 session factory 劃分交易,而無需特別注意,只要它使用 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 來驅動交易時,即使您只存取單個資料庫並且僅使用無狀態會話 bean 透過容器管理交易提供宣告式交易,您也同時依賴 EJB 容器和 JTA。直接以程式設計方式使用 JTA 也需要 Jakarta EE 環境。
Spring 驅動的交易可以與本機定義的 Hibernate SessionFactory
以及本機 JDBC DataSource
一起運作,前提是它們存取單個資料庫。因此,只有在您有分散式交易需求時,才需要使用 Spring 的 JTA 交易策略。JCA 連接器需要容器特定的部署步驟,並且 (顯然) 首先需要 JCA 支援。此組態比部署具有本機資源定義和 Spring 驅動交易的簡單 Web 應用程式需要更多工作。
總之,如果您不使用 EJBs,請堅持使用本機 SessionFactory
設定和 Spring 的 HibernateTransactionManager
或 JtaTransactionManager
。您可以獲得所有優點,包括適當的交易式 JVM 層級快取和分散式交易,而無需容器部署的不便。僅當與 EJB 結合使用時,透過 JCA 連接器註冊 Hibernate SessionFactory
的 JNDI 才具有價值。
使用 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。 -
您也可以明確配置 Hibernate 的 JTA 相關屬性,特別是在
LocalSessionFactoryBean
的 "hibernateProperties" 中配置 "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 的回呼,透過 Hibernate 的
afterTransactionCompletion
回呼(用於清除 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
回呼來呼叫,並且可以正確清除其快取。