Spring ORM數據訪問——Hibernate

Hibernate

我們將首先介紹Spring環境中的Hibernate 5,然後介紹使用Hibernate 5來演示Spring集成O-R映射器的方法。本節將詳細介紹許多問題,並顯示DAO實現和事務劃分的不同變體。這些模式中大多數可以直接轉換爲所有其他支持的ORM工具。本章中的以下部分將通過簡單的例子來介紹其他ORM技術。

從Spring 5.0開始,Spring需要Hibernate ORM 4.3或更高版本的JPA支持,甚至Hibernate ORM 5.0+可以針對本機Hibernate Session API進行編程。請注意,Hibernate團隊可能不會在5.0之前維護任何版本,僅僅專注於5.2以後的版本。

在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。但是,EJB上下文通常不常見。

基於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支持任何Spring事務策略的Hibernate的SessionFactory.getCurrentSession()方法,即使使用HibernateTransactionManager返回當前的Spring管理的事務Session。當然,該方法的標準行爲仍然返回與正在進行的JTA事務相關聯的當前Session(如果有的話)。無論開發者是使用Spring的JtaTransactionManager,EJB容器管理事務(CMT)還是JTA,都會適用此行爲。

總而言之:開發者可以基於純Hibernate API來實現DAO,同時也可以參與Spring管理的事務。

聲明式事務劃分

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>

編程式事務劃分

開發者可以在應用程序的更高級別上對事務進行標定,在這樣的低級別數據訪問服務之上跨越任意數量的操作。而不對業務服務的實現進行限制;它只需要定義一個Spring的PlatformTransactionManager。當然,PlatformTransactionManager可以來自任何地方,但最好是通過setTransactionManager(..)方法以Bean來注入,正如ProductDAO應該由setProductDao(..)方法配置一樣。下面的代碼顯示Spring應用程序上下文中的事務管理器和業務服務定義,以及業務方法實現的示例:

<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也是一樣的處理邏輯,但是同時還允許基於方法配置回滾策略。

事務管理策略

無論是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屬性的配置以確定公開事務的類型。

對比容器管理的和本地定義的資源

開發者可以在不修改一行代碼的情況下,在容器管理的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事務代碼,並將TransactionManagerLookup配置爲正確的JVM級緩存。

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才有價值。

Hibernate的虛假應用服務器警告

In some JTA environments with very strict XADataSource implementations — currently only some WebLogic Server and WebSphere versions — when Hibernate is configured without regard to the JTA PlatformTransactionManager object for that environment, it is possible for spurious warning or exceptions to show up in the application server log. These warnings or exceptions indicate that the connection being accessed is no longer valid, or JDBC access is no longer valid, possibly because the transaction is no longer active. As an example, here is an actual exception from WebLogic:

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

java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
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方法回調事務,可以正確清除其緩存。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章