Hibernate的Lazy初始化1:n關系時,你必須保證是在同一個Session內部使用這個關系集合,不然Hiernate將拋出例外。
另外,你不愿意你的DAO測試代碼每次都打開關系Session,因此,我們一般會采用OpenSessionInView模式。
OpenSessionInViewFilter解決Web應用程序的問題
如果程序是在正常的Web程序中運行,那么Spring的
OpenSessionInViewFilter能夠解決問題,它:
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);
}
}
可以看到,這個Filter在request開始之前,把sessionFactory綁定到TransactionSynchronizationManager,和這個SessionHolder相關。這個意味著所有request執行過程中將使用這個session。而在請求結束后,將和這個sessionFactory對應的session解綁,并且關閉Session。
為什么綁定以后,就可以防止每次不會新開一個Session呢?看看HibernateDaoSupport的情況:
public final void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
protected final HibernateTemplate getHibernateTemplate() {
return hibernateTemplate;
}
我們的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
public Object execute(HibernateCallback action) throws DataAccessException {
Session session = (!this.allowCreate ?
SessionFactoryUtils.getSession(getSessionFactory(),
false) :
SessionFactoryUtils.getSession(getSessionFactory(),
getEntityInterceptor(),
getJdbcExceptionTranslator()));
boolean existingTransaction =
TransactionSynchronizationManager.hasResource(getSessionFactory());
if (!existingTransaction && getFlushMode() == FLUSH_NEVER) {
session.setFlushMode(FlushMode.NEVER);
}
try {
Object result = action.doInHibernate(session);
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 {
SessionFactoryUtils.closeSessionIfNecessary(
session, getSessionFactory());
}
}
而這個SessionFactoryUtils能否得到當前的session以及closeSessionIfNecessary是否真正關閉session,端取決于這個session是否用sessionHolder和這個sessionFactory在我們最開始提到的TransactionSynchronizationManager綁定。
public static void closeSessionIfNecessary(Session session,
SessionFactory sessionFactory)
throws CleanupFailureDataAccessException {
if (session == null ||
TransactionSynchronizationManager.hasResource(sessionFactory)) {
return;
}
logger.debug("Closing Hibernate session");
try {
session.close();
}
catch (JDBCException ex) {
// SQLException underneath
throw new CleanupFailureDataAccessException(
"Cannot close Hibernate session", ex.getSQLException());
}
catch (HibernateException ex) {
throw new CleanupFailureDataAccessException(
"Cannot close Hibernate session", ex);
}
}
HibernateInterceptor和OpenSessionInViewInterceptor的問題
使用同樣的方法,這兩個Interceptor可以用來解決問題。但是關鍵的不同之處在于,它們的力度只能定義在DAO或業務方法上,而不是在我們的Test方法上,除非我們把它們應用到TestCase的方法上,但你不大可能為TestCase去定義一個接口,然后把Interceptor應用到這個接口的某些方法上。直接使用HibernateTransactionManager也是一樣的。因此,如果我們有這樣的測試:
Category parentCategory = new Category ();
parentCategory.setName("parent");
dao.save(parentCategory); Category childCategory = new Category();
childCategory.setName("child");
parentCategory.addChild(childCategory);
dao.save(childCategory);
Category savedParent = dao.getCategory("parent");
Category savedChild = (Category ) savedParent.getChildren().get(0);
assertEquals(savedChild, childCategory);
將意味著兩件事情:
- 每次DAO執行都會啟動一個session和關閉一個session
- 如果我們定義了一個lazy的關系,那么最后的Category savedChild = (Category ) savedParent.getChildren().get(0);將會讓hibernate報錯。
解決方案
一種方法是對TestCase應用Interceptor或者TransactionManager,但這個恐怕會造成很多麻煩。除非是使用增強方式的AOP.我前期采用這種方法(Aspectwerkz),在Eclipse里面也跑得含好。
另一種方法是在TestCase的setup和teardown里面實現和Filter完全一樣的處理,其他的TestCase都從這個TestCase繼承,這種方法是我目前所使用的。