Hibernate

我們首先介紹 Hibernate 5 在 Spring 環境中的應用,以此演示 Spring 在整合 OR 對映器方面所採取的方法。本節詳細討論了許多問題,並展示了 DAO 實現和事務劃分的不同變體。這些模式中的大多數都可以直接應用於所有其他受支援的 ORM 工具。本章後面的部分將介紹其他 ORM 技術並提供簡短示例。

自 Spring Framework 6.0 起,Spring 要求 Hibernate ORM 5.5+ 用於 Spring 的 HibernateJpaVendorAdapter 以及原生的 Hibernate SessionFactory 配置。我們推薦將 Hibernate ORM 5.6 作為該 Hibernate 生成中的最後一個特性分支。

Hibernate ORM 6.x 僅作為 JPA 提供程式(HibernateJpaVendorAdapter)受支援。使用 orm.hibernate5 包的原生 SessionFactory 配置不再受支援。我們建議新開發專案使用 Hibernate ORM 6.1/6.2 及 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 還提供了一個 LocalSessionFactoryBuilder 變體,它與 @Bean 風格的配置和程式設計式設定(不涉及 FactoryBean)無縫整合。

LocalSessionFactoryBeanLocalSessionFactoryBuilder 都支援後臺引導(bootstrapping),Hibernate 初始化在一個給定的引導執行器(例如 SimpleAsyncTaskExecutor)上與應用程式引導執行緒並行執行。在 LocalSessionFactoryBean 上,這透過 bootstrapExecutor 屬性可用。在程式設計式的 LocalSessionFactoryBuilder 上,有一個過載的 buildSessionFactory 方法接受一個引導執行器引數。

這種原生的 Hibernate 設定也可以暴露一個 JPA EntityManagerFactory,用於標準的 JPA 互動以及原生的 Hibernate 訪問。詳情請參閱JPA 的原生 Hibernate 設定

基於原生 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 表現相同,但允許根據每個方法配置回滾策略。

事務管理策略

TransactionTemplateTransactionInterceptor 都將實際的事務處理委託給 PlatformTransactionManager 例項(對於單個 Hibernate SessionFactory,可以是使用底層 ThreadLocal SessionHibernateTransactionManager),或者對於 Hibernate 應用程式,可以是 JtaTransactionManager(委託給容器的 JTA 子系統)。您甚至可以使用自定義的 PlatformTransactionManager 實現。從原生 Hibernate 事務管理切換到 JTA(例如,當您的應用程式的某些部署面臨分散式事務需求時)僅僅是配置問題。您可以將 Hibernate 事務管理器替換為 Spring 的 JTA 事務實現。事務劃分和資料訪問程式碼無需更改,因為它們使用通用的事務管理 API。

對於跨多個 Hibernate session factory 的分散式事務,您可以將 JtaTransactionManager 作為事務策略與多個 LocalSessionFactoryBean 定義結合使用。然後,每個 DAO 會將其對應的 bean 屬性傳遞一個特定的 SessionFactory 引用。如果所有底層 JDBC 資料來源都是事務性的容器資料來源,則業務服務可以在任意數量的 DAO 和任意數量的 session factory 之間劃分事務,而無需特別考慮,只要它使用 JtaTransactionManager 作為策略即可。

HibernateTransactionManagerJtaTransactionManager 都允許使用 Hibernate 進行適當的 JVM 級別快取處理,無需特定於容器的事務管理器查詢或 JCA 聯結器(如果您不使用 EJB 啟動事務)。

HibernateTransactionManager 可以將 Hibernate JDBC Connection 匯出到用於特定 DataSource 的原生 JDBC 訪問程式碼。這種能力允許在混合使用 Hibernate 和 JDBC 資料訪問時進行高級別的事務劃分,完全無需 JTA,前提是您只訪問一個數據庫。如果您已透過 LocalSessionFactoryBean 類的 dataSource 屬性使用 DataSource 設定傳入的 SessionFactoryHibernateTransactionManager 會自動將 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 的 HibernateTransactionManagerJtaTransactionManager。您可以獲得所有好處,包括適當的事務性 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 回撥呼叫事務,並且可以正確清除其快取。