1. 程式人生 > >open session and Hibernate事務處理機制

open session and Hibernate事務處理機制

在沒有使用Spring提供的Open Session In View情況下,因需要在service(or Dao)層裡把session關閉,所以lazy loading true的話,要在應用層內把關係集合都初始化,如 company.getEmployees(),否則Hibernatesession already closed Exception;    Open Session In View提供了一種簡便的方法,較好地解決了lazy loading問題.

它有兩種配置方式OpenSessionInViewInterceptorOpenSessionInViewFilter(具體參看

SpringSide),功能相同,只是一個在web.xml配置,另一個在application.xml配置而已。

    Open Session In Viewrequestsession繫結到當前thread期間一直保持hibernate sessionopen狀態,使sessionrequest的整個期間都可以使用,如在View層裡PO也可以lazy loading資料,如 ${ company.employees }。當View 層邏輯完成後,才會通過FilterdoFilter方法或InterceptorpostHandle方法自動關閉session

OpenSessionInViewInterceptor配置
  1. <beans>
  2. <bean name="openSessionInViewInterceptor"
  3. class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor">
  4. <property name="sessionFactory">
  5. <ref bean="sessionFactory"/>
  6. </property>
  7. </bean>
  8. <bean id="urlMapping"
  9. class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"
    >
  10. <property name="interceptors">
  11. <list>
  12. <ref bean="openSessionInViewInterceptor"/>
  13. </list>
  14. </property>
  15. <property name="mappings">
  16. ...
  17. </property>
  18. </bean>
  19. ...
  20. </beans>
OpenSessionInViewFilter配置
  1. <web-app>
  2. ...
  3. <filter>
  4. <filter-name>hibernateFilter</filter-name>
  5. <filter-class>
  6. org.springframework.orm.hibernate3.support.OpenSessionInViewFilter
  7. </filter-class>
  8. <!-- singleSession預設為true,若設為false則等於沒用OpenSessionInView -->
  9. <init-param>
  10. <param-name>singleSession</param-name>
  11. <param-value>true</param-value>
  12. </init-param>
  13. </filter>
  14. ...
  15. <filter-mapping>
  16. <filter-name>hibernateFilter</filter-name>
  17. <url-pattern>*.do</url-pattern>
  18. </filter-mapping>
  19. ...
  20. </web-app>

很多人在使用OpenSessionInView過程中提及一個錯誤:

  1. org.springframework.dao.InvalidDataAccessApiUsageException: Write operations
  2. are not allowed in read-only mode (FlushMode.NEVER) - turn your Session into
  3. FlushMode.AUTO or remove 'readOnly' marker from transaction definition

看看OpenSessionInViewFilter裡的幾個方法

  1. protected void doFilterInternal(HttpServletRequest request,
    HttpServletResponse response,FilterChain filterChain)
    throws ServletException, IOException
    {
     SessionFactory sessionFactory = lookupSessionFactory();
     logger.debug("Opening Hibernate Session in OpenSessionInViewFilter");
     Session session = getSession(sessionFactory);
     TransactionSynchronizationManager.bindResource
    (
      sessionFactory, new SessionHolder(session));
     try
    {
      filterChain.doFilter(request, response);
     
    }
     finally
    {
     TransactionSynchronizationManager.unbindResource(sessionFactory);
     logger.debug("Closing Hibernate Session in OpenSessionInViewFilter");
     closeSession(session, sessionFactory);
     
    }
    }
  2. protected Session getSession(SessionFactory sessionFactory)
    throws DataAccessResourceFailureException
    {
     Session session = SessionFactoryUtils.getSession(sessionFactory, true);
     session.setFlushMode(FlushMode.NEVER);
     return session;
    }

  3. protected
    void closeSession(Session session, SessionFactory sessionFactory)
    throws CleanupFailureDataAccessException
    {
     SessionFactoryUtils.closeSessionIfNecessary(session, sessionFactory);
    }

           關於繫結session的方式,通過看spring裡TransactionSynchronizationManager的實現,發現:它維護一個java.lang.ThreadLocal型別的resources,resources負責持有執行緒區域性變數,這裡resources持有的是一個HashMap,通過TransactionSynchronizationManager.bindResource()方法在map裡繫結和執行緒相關的所有變數到他們的標識上,包括如上所述的繫結在sessionFactory上的執行緒區域性session。sessionHolder只不過是存放可以hold一個session並可以和transtaction同步的容器。可以看到OpenSessionInViewFilter在getSession的時候,會把獲取回來的session的flush mode 設為FlushMode.NEVER。然後把該sessionFactory繫結到TransactionSynchronizationManager,使request的整個過程都使用同一個session,在請求過後再接除該sessionFactory的繫結,最後closeSessionIfNecessary根據該session是否已和transaction繫結來決定是否關閉session。繫結以後,就可以防止每次不會新開一個Session呢?看看HibernateDaoSupport的情況:

        我們的DAO將使用這個template進行操作.

public abstract class BaseHibernateObjectDao extends HibernateDaoSupport
implements BaseObjectDao {
     protected BaseEntityObject getByClassId(final long id) {
                BaseEntityObject obj =(BaseEntityObject) getHibernateTemplate().execute(new HibernateCallback() {
                        public Object doInHibernate(Session session) throws HibernateException {
                                    return session.get(getPersistentClass(),new Long(id));
                        }
                });
                return obj;
      }
     public void save(BaseEntityObject entity) {
                  getHibernateTemplate().saveOrUpdate(entity);
     }

    public void remove(BaseEntityObject entity) {
              try {
                     getHibernateTemplate().delete(entity);
              } catch (Exception e) {
                      throw new FlexEnterpriseDataAccessException(e);
             }
     }

      public void refresh(final BaseEntityObject entity) {
               getHibernateTemplate().execute(new HibernateCallback() {
                          public Object doInHibernate(Session session) throws HibernateException {
                                      session.refresh(entity);
                                      return null;
                          }
               });
      }

     public void replicate(final Object entity) {
                getHibernateTemplate().execute(new HibernateCallback() {
                          public Object doInHibernate(Session session)throws HibernateException {
                                      session.replicate(entity,ReplicationMode.OVERWRITE);
                                      return null;
               }
                });
      }
}

        而HibernateTemplate試圖每次在execute之前去獲得Session,執行完就力爭關閉Session

      而這個SessionFactoryUtils能否得到當前的session以及closeSessionIfNecessary是否真正關閉session,端取決於這個session是否用sessionHolder和這個sessionFactory在我們最開始提到的TransactionSynchronizationManager繫結。

     public static void closeSessionIfNecessary(Session session, SessionFactory sessionFactory)

  1. throws CleanupFailureDataAccessException {
  2. if (session == null ||
    TransactionSynchronizationManager.hasResource(sessionFactory)) {
  3. return;
  4. }
  5. logger.debug("Closing Hibernate session");
  6. try {
  7. session.close();
  8. }
  9. catch (JDBCException ex) {
  10. // SQLException underneath
  11. throw new CleanupFailureDataAccessException("Could not close Hibernate session", ex.getSQLException());
  12. }
  13. catch (HibernateException ex) {
  14. throw new CleanupFailureDataAccessException("Could not close Hibernate session", ex);
  15. }
  16. }

    在這個過程中,若HibernateTemplate 發現自當前session有不是readOnly的transaction,就會獲取到FlushMode.AUTO Session,使方法擁有寫許可權。也即是,如果有不是readOnly的transaction就可以由Flush.NEVER轉為Flush.AUTO,擁有insert,update,delete操作許可權,如果沒有transaction,並且沒有另外人為地設flush model的話,則doFilter的整個過程都是Flush.NEVER。所以受transaction保護的方法有寫許可權,沒受保護的則沒有。

  1. 可能的解決方式有:
    1、將singleSession設為false,這樣只要改web.xml,缺點是Hibernate Session的Instance可能會大增,使用的JDBC Connection量也會大增,如果Connection Pool的maxPoolSize設得太小,很容易就出問題。
    2、在控制器中自行管理Session的FlushMode,麻煩的是每個有Modify的Method都要多幾行程式。
          session.setFlushMode(FlushMode.AUTO);
          session.update(user);
          session.flush();
    3、Extend OpenSessionInViewFilter,Override protected Session getSession(SessionFactory sessionFactory),將FlushMode直接改為Auto。
    4、讓方法受Spring的事務控制。這就是常使用的方法:

採用spring的事務宣告,使方法受transaction控制

  1.   <bean id="baseTransaction"
    class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
              abstract="true">
            <property name="transactionManager" ref="transactionManager"/>
            <property name="proxyTargetClass" value="true"/>
            <property name="transactionAttributes">
                <props>
                    <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                    <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
                    <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
                    <prop key="save*">PROPAGATION_REQUIRED</prop>
                    <prop key="add*">PROPAGATION_REQUIRED</prop>
                    <prop key="update*">PROPAGATION_REQUIRED</prop>
                    <prop key="remove*">PROPAGATION_REQUIRED</prop>
                </props>
            </property>
        </bean>
  2.     <bean id="userService" parent="baseTransaction">
            <property name="target">
                <bean class="com.phopesoft.security.service.impl.UserServiceImpl"/>
            </property>
        </bean>

對於上例,則以save,add,update,remove開頭的方法擁有可寫的事務,如果當前有某個方法,如命名為importExcel(),則因沒有transaction而沒有寫許可權,這時若方法內有insert,update,delete操作的話,則需要手動設定flush model為Flush.AUTO,如

  1. session.setFlushMode(FlushMode.AUTO);
  2. session.save(user);
  3. session.flush();

     儘管Open Session In View看起來還不錯,其實副作用不少。看回上面OpenSessionInViewFilter的doFilterInternal方法程式碼,這個方法實際上是被父類的doFilter呼叫的,因此,我們可以大約瞭解的OpenSessionInViewFilter呼叫流程: request(請求)->open session並開始transaction->controller->View(Jsp)->結束transaction並close session.

     一切看起來很正確,尤其是在本地開發測試的時候沒出現問題,但試想下如果流程中的某一步被阻塞的話,那在這期間connection就一直被佔用而不釋放。最有可能被阻塞的就是在寫Jsp這步,一方面可能是頁面內容大,response.write的時間長,另一方面可能是網速慢,伺服器與使用者間傳輸時間久。當大量這樣的情況出現時,就有連線池連線不足,造成頁面假死現象。

Open Session In View是個雙刃劍,放在公網上內容多流量大的網站請慎用。

另外:這樣會產生一點危險性,畢竟把資料庫訪問的環境放到了表現層。(:用VO)

        Hibernate是對JDBC的輕量級物件封裝,Hibernate本身是不具備Transaction處理功能的,Hibernate的Transaction實際上是底層的JDBC Transaction的封裝,或者是JTA Transaction的封裝,下面我們詳細的分析:

  Hibernate可以配置為JDBCTransaction或者是JTATransaction,這取決於你在hibernate.properties中的配置:

#hibernate.transaction.factory_class
net.sf.hibernate.transaction.JTATransactionFactory
#hibernate.transaction.factory_class
net.sf.hibernate.transaction.JDBCTransactionFactory

  如果你什麼都不配置,預設情況下使用JDBCTransaction,如果你配置為:

hibernate.transaction.factory_class
net.sf.hibernate.transaction.JTATransactionFactory

  將使用JTATransaction,不管你準備讓Hibernate使用JDBCTransaction,還是JTATransaction,我的忠告就是什麼都不配,將讓它保持預設狀態,如下:

#hibernate.transaction.factory_class
net.sf.hibernate.transaction.JTATransactionFactory
#hibernate.transaction.factory_class
net.sf.hibernate.transaction.JDBCTransactionFactory

  在下面的分析中我會給出原因。

  一、JDBC Transaction

  看看使用JDBC Transaction的時候我們的程式碼例子:

Session session = sf.openSession();
Transaction tx = session.beginTransactioin();
...
session.flush();
tx.commit();
session.close();
  這是預設的情況,當你在程式碼中使用Hibernate的Transaction的時候實際上就是JDBCTransaction。那麼JDBCTransaction究竟是什麼東西呢?來看看原始碼就清楚了:

  Hibernate2.0.3原始碼中的類

  net.sf.hibernate.transaction.JDBCTransaction:

public void begin() throws HibernateException {
...
if (toggleAutoCommit) session.connection().setAutoCommit(false);
...
}
  這是啟動Transaction的方法,看到 connection().setAutoCommit(false) 了嗎?是不是很熟悉?

  再來看

public void commit() throws HibernateException {
...
try {
if ( session.getFlushMode()!=FlushMode.NEVER ) session.flush();
try {
session.connection().commit();
committed = true;
}
...
toggleAutoCommit();
}

  這是提交方法,看到connection().commit() 了嗎?下面就不用我多說了,這個類程式碼非常簡單易懂,通過閱讀使我們明白Hibernate的Transaction都在幹了些什麼?我現在把用Hibernate寫的例子翻譯成JDBC,大家就一目瞭然了:

Connection conn = ...;               <--- session = sf.openSession();
conn.setAutoCommit(false);   <--- tx = session.beginTransactioin();
... <--- ...
conn.commit();                           <--- tx.commit(); (對應左邊的兩句)
conn.setAutoCommit(true);
conn.close();                              <--- session.close();

  看明白了吧,Hibernate的JDBCTransaction根本就是conn.commit而已,根本毫無神祕可言,只不過在Hibernate中,Session開啟的時候,就會自動conn.setAutoCommit(false),不像一般的JDBC,預設都是true,所以你最後不寫commit也沒有關係,由於Hibernate已經把AutoCommit給關掉了,所以用Hibernate的時候,你在程式中不寫Transaction的話,資料庫根本就沒有反應。 

 二、JTATransaction

如果你在EJB中使用Hibernate,或者準備用JTA來管理跨Session的長事務,那麼就需要使用JTATransaction,先看一個例子:

javax.transaction.UserTransaction tx = new InitialContext().lookup("javax.transaction.UserTransaction");

Session s1 = sf.openSession();
...
s1.flush();
s1.close();
...

Session s2 = sf.openSession();
...
s2.flush();
s2.close();

tx.commit();


這是標準的使用JTA的程式碼片斷,Transaction是跨Session的,它的生命週期比Session要長。如果你在EJB中使用Hibernate,那麼是最簡單不過的了,你什麼Transaction程式碼統統都不要寫了,直接在EJB的部署描述符上配置某某方法是否使用事務就可以了。

現在我們來分析一下JTATransaction的原始碼, net.sf.hibernate.transaction.JTATransaction:

public void begin(InitialContext context, ...
...
ut = (UserTransaction) context.lookup(utName);
...


看清楚了嗎? 和我上面寫的程式碼 tx = new Initial Context?().lookup("javax.transaction.UserTransaction"); 是不是完全一樣?

public void commit() ...
...
if (newTransaction) ut.commit();
...


JTATransaction的控制稍微複雜,不過仍然可以很清楚的看出來Hibernate是如何封裝JTA的Transaction程式碼的。

但是你現在是否看到了什麼問題? 仔細想一下,Hibernate Transaction是從Session中獲得的,tx = session.beginTransaction(),最後要先提交tx,然後再session.close,這完全符合JDBC的Transaction的操作順序,但是這個順序是和JTA的Transactioin操作順序徹底矛盾的!!! JTA是先啟動Transaction,然後啟動Session,關閉Session,最後提交Transaction,因此當你使用JTA的Transaction的時候,那麼就千萬不要使用Hibernate的Transaction,而是應該像我上面的JTA的程式碼片斷那樣使用才行。

總結:
1、在JDBC上使用Hibernate

必須寫上Hibernate Transaction程式碼,否則資料庫沒有反應。此時Hibernate的Transaction就是Connection.commit而已

2、在JTA上使用Hibernate

寫JTA的Transaction程式碼,不要寫Hibernate的Transaction程式碼,否則程式會報錯

3、在EJB上使用Hibernate

什麼Transactioin程式碼都不要寫,在EJB的部署描述符裡面配置

|---CMT(Container Managed Transaction)
|
|---BMT(Bean Managed Transaction)
|
|----JDBC Transaction
|
|----JTA Transaction

關於session:

1.  servlet的session機制基於cookies,關閉瀏覽器的cookies則session失效即不能用網站的登入功能。

2.  Hibernate Session.

      1>. session 清理快取時,按照以下順序執行SQL語句:

            session.save()的實體insert

實體的update

            對集合的delete

            集合元素的delete,update,insert

            集合的insert

            session.delete()的先後,執行實體的delete

       2>. 預設時,session在以下時間點清理快取:

              net.sf.hibernate.Transaction.commit():先清理快取,再向資料庫提交事務                                                          

              Session.find()或iterate()時,若快取中持久化物件的屬性發生了變化,就會先清快取,以保證查詢結果正確

       3>.  Session的commit()和flush()的區別:flush()只執行SQL語句,不提交事務;commit()先呼叫flush(),再提交事務

       4>.  Session.setFlushMode()用於設定清理快取的時間點。

清理快取的模式 Session的查詢方法 Session.commit() Session.flush()
FlushMode.AUTO 清理 清理 清理
FlushMode.COMMIT 不清理 清理 清理
FlushMode.NEVER 不清理 不清理 清理