2011年4月27日 星期三

Spring + Hibernate , Session & Transaction

Spring + Hibernate 整合的時候, 一般設計上會分Service Layer, Dao Layer, 在Service method 執行階段去設定Transaction , 這邊是有一種Project 在設計上造成Hibernate SessionException,
"Session was already closed"
出錯的問題在於DAO的實作都用this.getSession() ,(All Dao Layer class extends HibernateDaoSupport)
來取得hibernate Session ,但Service & DAO 的Reference 關系很復雜。
(Spring 的文件中提到, spring 的Session是一個thread-bound Session,它是和某個Thread绑定的,而這個Thread往往就是載入Servlet/Jsp的那的thread,實際的意思就是其生命周期scope是request/response的。
getSession取得了Hibernate的Session,這個Session可能是當前request中之前使用過的,也可能是一個新的,this.getSession()就有可能造成transaction 在commit的時候closed session出錯)


雖然繼承了HibernateDaoSupport這個類,但是this.getSession(),獲得的session也要在使用後關閉,因為這個session是原生的session不是經過sping代理過的,並且還沒有Transaction,自動提交,自動關閉連接等功能,所以使用使用getSession()獲得session時一定要關閉。

Hibernate documentation 提到的 Session Object
A Session is an inexpensive, non-threadsafe object that should be used once and then discarded for: a single request, a conversation or a single unit of work. A Session will not obtain a JDBC Connection, or a Datasource, unless it is needed. It will not consume any resources until used.
Do not use the session-per-operation antipattern,
The most common pattern in a multi-user client/server application is session-per-request.


HiberDaoSupport代碼
/**
  * Obtain a Hibernate Session, either from the current transaction or
  * a new one. The latter is only allowed if "allowCreate" is true.
  * <p><b>Note that this is not meant to be invoked from HibernateTemplate code
  * but rather just in plain Hibernate code.</b> Either rely on a thread-bound
  * Session or use it in combination with {@link #releaseSession}.
  * <p>In general, it is recommended to use
  * {@link #getHibernateTemplate() HibernateTemplate}, either with
  * the provided convenience operations or with a custom
  * {@link org.springframework.orm.hibernate3.HibernateCallback} that
  * provides you with a Session to work on. HibernateTemplate will care
  * for all resource management and for proper exception conversion.
  * @param allowCreate if a non-transactional Session should be created when no
  * transactional Session can be found for the current thread
  * @return the Hibernate Session
  * @throws DataAccessResourceFailureException if the Session couldn't be created
  * @throws IllegalStateException if no thread-bound Session found and allowCreate=false
  * @see org.springframework.orm.hibernate3.SessionFactoryUtils#getSession(SessionFactory, boolean)
  */
protected final Session getSession(boolean allowCreate)
     throws DataAccessResourceFailureException, IllegalStateException {

  return (!allowCreate ?
      SessionFactoryUtils.getSession(getSessionFactory(), false) :
    SessionFactoryUtils.getSession(
      getSessionFactory(),
      this.hibernateTemplate.getEntityInterceptor(),
      this.hibernateTemplate.getJdbcExceptionTranslator()));
 }

allowCreate default 數值為true , this.getSession()能從當前的事務或新的事務取的一個新的Hibernate session object,this.getHibernateTemplate().getSessionFactory().getCurrentSession()/openSession()則從spring中獲取session
getCurrentSession()創建的Session會綁定到當前的線程中去、而採用OpenSession()則不會。
採用getCurrentSession()創建的Session在commit或rollback後會自動關閉,採用OpenSession()必須手動關閉。



看看HibernateTemplate
public List find(final String queryString, final Object... values) throws DataAccessException {
  return executeWithNativeSession(new HibernateCallback<List>() {
   public List doInHibernate(Session session) throws HibernateException {
    Query queryObject = session.createQuery(queryString);
    prepareQuery(queryObject);
    if (values != null) {
     for (int i = 0; i < values.length; i  ) {
      queryObject.setParameter(i, values[i]);
     }
    }
    return queryObject.list();
   }
  });
 }


/**
  * Execute the action specified by the given action object within a
  * native {@link org.hibernate.Session}.
  * <p>This execute variant overrides the template-wide
  * {@link #isExposeNativeSession() "exposeNativeSession"} setting.
  * @param action callback object that specifies the Hibernate action
  * @return a result object returned by the action, or <code>null</code>
  * @throws org.springframework.dao.DataAccessException in case of Hibernate errors
  */
 public <T> T executeWithNativeSession(HibernateCallback<T> action) {
  return doExecute(action, false, true);
 }



/**
  * Execute the action specified by the given action object within a Session.
  * @param action callback object that specifies the Hibernate action
  * @param enforceNewSession whether to enforce a new Session for this template
  * even if there is a pre-bound transactional Session
  * @param enforceNativeSession whether to enforce exposure of the native
  * Hibernate Session to callback code
  * @return a result object returned by the action, or <code>null</code>
  * @throws org.springframework.dao.DataAccessException in case of Hibernate errors
  */
 protected <T> T doExecute(HibernateCallback<T> action, boolean enforceNewSession, boolean enforceNativeSession)
   throws DataAccessException {

  Assert.notNull(action, "Callback object must not be null");

  Session session = (enforceNewSession ?
    SessionFactoryUtils.getNewSession(getSessionFactory(), getEntityInterceptor()) : getSession());
  boolean existingTransaction = (!enforceNewSession &&
    (!isAllowCreate() || SessionFactoryUtils.isSessionTransactional(session, getSessionFactory())));
  if (existingTransaction) {
   logger.debug("Found thread-bound Session for HibernateTemplate");
  }

  FlushMode previousFlushMode = null;
  try {
   previousFlushMode = applyFlushMode(session, existingTransaction);
   enableFilters(session);
   Session sessionToExpose =
     (enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session));
   T result = action.doInHibernate(sessionToExpose);
   flushIfNecessary(session, existingTransaction);
   return result;
  }
  catch (HibernateException ex) {
   throw convertHibernateAccessException(ex);
  }
  catch (SQLException ex) {
   throw convertJdbcAccessException(ex);
  }
  catch (RuntimeException ex) {
   // Callback code threw application exception...
   throw ex;
  }
  finally {
   if (existingTransaction) {
    logger.debug("Not closing pre-bound Hibernate Session after HibernateTemplate");
    disableFilters(session);
    if (previousFlushMode != null) {
     session.setFlushMode(previousFlushMode);
    }
   }
   else {
    // Never use deferred close for an explicitly new Session.
    if (isAlwaysUseNewSession()) {
     SessionFactoryUtils.closeSession(session);
    }
    else {
     SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, getSessionFactory());
    }
   }
  }
 }



再來重要的就是HibernateTransactionManager, 在doBegin 裡面, HibernateTransactionObject的一個實例,這個實例裡主要存放的就是sessionholder,sessionholder裡存放的就是開始事務的session and Transaction,如果之前沒有 sessionholder存放到thread,那麼這個 HibernateTransactionObject的實例的屬性其實是空的。
如果Transaction中並沒有存放sessionholder,那麼就新建一個session,放到新的sessionholder中,再放到HibernateTransactionObject的實例中。
如果給service設置聲明式Transaction,假設Transaction為required,然後一個service調用另一個service時,他們其實是共用一個session,原則是沒有就create,反之則不create,並返回之前已create的session和transaction。 也就是說spring通過threadlocal把s​​ession和對應的transaction放到線程之中,保證了在整個方法棧的任何一個地方都能得到同一個session和transaction。
所以如果你的方法在事務體之內,那麼你只要通過hibernatesupportdao或者hibernatetemplate來得到session的話,那這個session一定是開始事務的那個session,這個得到session的主要方法在SessionFactoryUtils裡。


HibernateTransactionManager
@Override
 protected void doBegin(Object transaction, TransactionDefinition definition) {
  HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;

  if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
   throw new IllegalTransactionStateException(
     "Pre-bound JDBC Connection found! HibernateTransactionManager does not support "  
     "running within DataSourceTransactionManager if told to manage the DataSource itself. "  
     "It is recommended to use a single HibernateTransactionManager for all transactions "  
     "on a single DataSource, no matter whether Hibernate or JDBC access.");
  }

  Session session = null;

  try {
   if (txObject.getSessionHolder() == null || txObject.getSessionHolder().isSynchronizedWithTransaction()) {
    Interceptor entityInterceptor = getEntityInterceptor();
    Session newSession = (entityInterceptor != null ?
      getSessionFactory().openSession(entityInterceptor) : getSessionFactory().openSession());
    if (logger.isDebugEnabled()) {
     logger.debug("Opened new Session ["   SessionFactoryUtils.toString(newSession)  
       "] for Hibernate transaction");
    }
    txObject.setSession(newSession);
   }

   session = txObject.getSessionHolder().getSession();

   if (this.prepareConnection && isSameConnectionForEntireSession(session)) {
    // We're allowed to change the transaction settings of the JDBC Connection.
    if (logger.isDebugEnabled()) {
     logger.debug(
       "Preparing JDBC Connection of Hibernate Session ["   SessionFactoryUtils.toString(session)   "]");
    }
    Connection con = session.connection();
    Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
    txObject.setPreviousIsolationLevel(previousIsolationLevel);
   }
   else {
    // Not allowed to change the transaction settings of the JDBC Connection.
    if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
     // We should set a specific isolation level but are not allowed to...
     throw new InvalidIsolationLevelException(
       "HibernateTransactionManager is not allowed to support custom isolation levels: "  
       "make sure that its 'prepareConnection' flag is on (the default) and that the "  
       "Hibernate connection release mode is set to 'on_close' (SpringTransactionFactory's default). "  
       "Make sure that your LocalSessionFactoryBean actually uses SpringTransactionFactory: Your "  
       "Hibernate properties should *not* include a 'hibernate.transaction.factory_class' property!");
    }
    if (logger.isDebugEnabled()) {
     logger.debug(
       "Not preparing JDBC Connection of Hibernate Session ["   SessionFactoryUtils.toString(session)   "]");
    }
   }

   if (definition.isReadOnly() && txObject.isNewSession()) {
    // Just set to NEVER in case of a new Session for this transaction.
    session.setFlushMode(FlushMode.MANUAL);
   }

   if (!definition.isReadOnly() && !txObject.isNewSession()) {
    // We need AUTO or COMMIT for a non-read-only transaction.
    FlushMode flushMode = session.getFlushMode();
    if (flushMode.lessThan(FlushMode.COMMIT)) {
     session.setFlushMode(FlushMode.AUTO);
     txObject.getSessionHolder().setPreviousFlushMode(flushMode);
    }
   }

   Transaction hibTx;

   // Register transaction timeout.
   int timeout = determineTimeout(definition);
   if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
    // Use Hibernate's own transaction timeout mechanism on Hibernate 3.1 
    // Applies to all statements, also to inserts, updates and deletes!
    hibTx = session.getTransaction();
    hibTx.setTimeout(timeout);
    hibTx.begin();
   }
   else {
    // Open a plain Hibernate transaction without specified timeout.
    hibTx = session.beginTransaction();
   }

   // Add the Hibernate transaction to the session holder.
   txObject.getSessionHolder().setTransaction(hibTx);

   // Register the Hibernate Session's JDBC Connection for the DataSource, if set.
   if (getDataSource() != null) {
    Connection con = session.connection();
    ConnectionHolder conHolder = new ConnectionHolder(con);
    if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
     conHolder.setTimeoutInSeconds(timeout);
    }
    if (logger.isDebugEnabled()) {
     logger.debug("Exposing Hibernate transaction as JDBC transaction ["   con   "]");
    }
    TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
    txObject.setConnectionHolder(conHolder);
   }

   // Bind the session holder to the thread.
   if (txObject.isNewSessionHolder()) {
    TransactionSynchronizationManager.bindResource(getSessionFactory(), txObject.getSessionHolder());
   }
   txObject.getSessionHolder().setSynchronizedWithTransaction(true);
  }

  catch (Exception ex) {
   if (txObject.isNewSession()) {
    try {
     if (session.getTransaction().isActive()) {
      session.getTransaction().rollback();
     }
    }
    catch (Throwable ex2) {
     logger.debug("Could not rollback Session after failed transaction begin", ex);
    }
    finally {
     SessionFactoryUtils.closeSession(session);
    }
   }
   throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex);
  }
 }



結論:
Session的使用維護上有幾種:
1.getCurrentSession() : 獲得當前會話中的session,該session有容器自行維護管理,Spring可以代理事務。
2.this.getSession() : 從當前的執行中獲得或create 一個hibernate的session,自己關閉,釋放連接資源。
3.openSession(); 調用函數自行create一個數據庫的連接,並將其打開,在使用Spring操作非查詢語句的請況下,Spring的transaction 對該session對像不起到Transaction 管理的作用,所以該session對象應自己關閉,釋放連接資源。
當使用Spring 去管理Hibernate Session的時候,DAO Extends HibernateDaoSupport 時,session的取得我們並不在乎,如果需要取得session做處理時,HibernateTemplate提供HibernateCallback,就是为了满足使用了HibernateTemplate的情况下,仍然需要直接訪問Session的狀況。

個人認為Service Layer,DAO Layer所做的事應該單一化, Service 針對Method 做Transaction , 一個Method 代表的應該是一個完整的Transaction動作,要Reference其他Service所做的事時應該在Controll 呼叫2個不同的Service method。而DAO 所做的事應該是單純的DB動作。

Notes:

ServiceA 有個method 叫 find(id) , @Transactional(propagation=Propagation.REQUIRED)
ServiceB 也有個method 叫 find(id) , @Transactional(propagation=Propagation.REQUIRED)
然後在ServiceA 的find(id) 中 call ServiceB 的find(id)
Spring Log如下
Ts-[] ,15:24:47 [DEBUG] AbstractPlatformTransactionManager.java:370 - Creating new transaction with name [com.service.UserServiceImpl.find]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
  Ts-[] ,15:24:47 [DEBUG] HibernateTransactionManager.java:493 - Opened new Session [org.hibernate.impl.SessionImpl@940f82] for Hibernate transaction
  Ts-[] ,15:24:47 [DEBUG] HibernateTransactionManager.java:504 - Preparing JDBC Connection of Hibernate Session [org.hibernate.impl.SessionImpl@940f82]
  Ts-[] ,15:24:47 [DEBUG] DriverManagerDataSource.java:162 - Creating new JDBC DriverManager Connection to [jdbc:mysql://127.0.0.1:3306/test]
  Ts-[] ,15:24:47 [DEBUG] HibernateTransactionManager.java:569 - Exposing Hibernate transaction as JDBC transaction [com.mysql.jdbc.JDBC4Connection@92668c]
15:24:47 [DEBUG] HibernateTransactionManager.java:437 - Found thread-bound Session [org.hibernate.impl.SessionImpl@940f82] for Hibernate transaction
  Ts-[] ,15:24:47 [DEBUG] AbstractPlatformTransactionManager.java:468 - Participating in existing transaction
[DEBUG] AbstractPlatformTransactionManager.java:729 - Initiating transaction commit
  Ts-[] ,15:24:47 [DEBUG] HibernateTransactionManager.java:652 - Committing Hibernate transaction on Session [org.hibernate.impl.SessionImpl@940f82]
  Ts-[] ,15:24:47 [DEBUG] HibernateTransactionManager.java:734 - Closing Hibernate Session [org.hibernate.impl.SessionImpl@940f82] after transaction
  Ts-[] ,15:24:47 [DEBUG] SessionFactoryUtils.java:784 - Closing Hibernate Session
設為 REQUIRED - Support a current transaction, create a new one if none exists.
詳見Spring API doc





See also:

Hibernate Documentation

Spring Documentation

Tutorial:Create Spring 3 MVC Hibernate 3 Example

沒有留言:

張貼留言