1. 程式人生 > >《Spring 5 官方文件》16.ORM和資料訪問

《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的DataSourceHibernateSessionFactory的:

<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通過使用ThreadLocalSession),也可以是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資料訪問進行高級別事務劃分。如果開發者已經通過LocalSessionFactoryBeandataSource屬性與DataSource設定了傳入的SessionFactoryHibernateTransactionManager會自動將Hibernate事務公開為JDBC事務。或者,開發者可以通過HibernateTransactionManagerdataSource屬性的配置以確定公開事務的型別。

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的HibernateTransactionManagerJtaTransactionManager。開發者能夠得到了前面提到的所有好處,包括適當的事務性JVM級快取和分散式事務支援,而且沒有容器部署的不便。只有必須配合EJB使用的時候,JNDI通過JCA聯結器來註冊HibernateSessionFactory才有價值。

16.3.7Hibernate的虛假應用伺服器警告

在某些具有非常嚴格的XADataSource實現的JTA環境(目前只有一些WebLogic Server和WebSphere版本)中,當配置Hibernate時,沒有考慮到JTA的 PlatformTransactionManager物件,可能會在應用程式伺服器日誌中顯示虛假警告或異常。這些警告或異常經常描述正在訪問的連線不再有效,或者JDBC訪問不再有效。這通常可能是因為事務不再有效。例如,這是WebLogic的一個實際異常:

  1. java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
  2. further JDBC access is allowed within this transaction.

開發者可以通過配置令Hibernate意識到Spring中同步的JTAPlatformTransactionManager例項的存在,這樣即可消除掉前面所說的虛假警告資訊。開發者有以下兩種選擇:

  • 如果在應用程式上下文中,開發者已經直接獲取了JTA PlatformTransactionManager物件(可能是從JNDI到JndiObjectFactoryBean或者<jee:jndi-lookup>標籤),並將其提供給Spring的JtaTransactionManager(其中最簡單的方法就是指定一個引用bean將此JTA PlatformTransactionManager例項定義為LocalSessionFactoryBeanjtaTransactionManager屬性的值)。 Spring之後會令PlatformTransactionManager物件對Hibernate可見。
  • 更有可能開發者無法獲取JTAPlatformTransactionManager例項,因為Spring的JtaTransactionManager是可以自己找到該例項的。因此,開發者需要配置Hibernate令其直接查詢JTA PlatformTransactionManager。開發者可以如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方法回撥事務,可以正確清除其快取。