众所周知, 为了解决 Hibernate Lazy 问题, Spring 中引入了 OpenSessionInViewInterceptor, 这样虽然解决了页面上的 Lazy Load 问题,却增加了各层之间的偶合性,
如果一个 Lazy 的 Collection 在页面上可以被正确的 load, 但是如果请求不是来自于 HttpServletRequest (比如在 TestCase 或 Service 中希望获取 lazy 的属性),
一般会导致两种错误:
代码
[java] view plaincopy
01.1. 设置了 lazy = "true"
02. 会导致 org.hibernate.LazyInitializationException: failed to lazily initialize a collection of xxx: xxx - no session or session was closed
03.2. 设置里 lazy = "false"
04. 会导致 org.hibernate.LazyInitializationException: could not initialize proxy - the owning Session was closed
为了方便测试, 灵活使用 lazy load, 我按照 OpenSessionInViewInterceptor 的思路实现了一个 HibernateLazyResolber, 代码如下:
[java] view plaincopy
package cn.ccut312.common.daobase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* <class>HibernateLazyResolver</class> 用于模拟 OpenSessionInViewInterceptor, 它可以被任意使用而不依赖于 Web 环境
*
* @see org.springframework.orm.hibernate.support.OpenSessionInViewInterceptor
* @see org.springframework.orm.hibernate.support.OpenSessionInViewFilter
* @since --
* @author 王政
* @version $Id: HibernateLazyResolver.java,v . // :: Administrator Exp $
*/
public class HibernateLazyResolver implements InitializingBean {
private static Log logger = LogFactory.getLog(HibernateLazyResolver.class);
private boolean singleSession = true;
private SessionFactory sessionFactory;
boolean participate = false;
protected Session session = null;
public final void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
/**
* Set whether to use a single session for each request. Default is true.
* <p>If set to false, each data access operation or transaction will use
* its own session (like without Open Session in View). Each of those
* sessions will be registered for deferred close, though, actually
* processed at request completion.
* @see SessionFactoryUtils#initDeferredClose
* @see SessionFactoryUtils#processDeferredClose
*/
public void setSingleSession(boolean singleSession) {
this.singleSession = singleSession;
}
/**
* Return whether to use a single session for each request.
*/
protected boolean isSingleSession() {
return singleSession;
}
public void afterPropertiesSet() throws Exception {
if (sessionFactory == null) {
throw new IllegalArgumentException("SessionFactory is reqirued!");
}
}
/**
* 初始化 session, 在需要 lazy 的开始处调用
*
*/
public void openSession() {
if (isSingleSession()) {
// single session mode
if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
// Do not modify the Session: just set the participate flag.
participate = true;
}
else {
logger.debug("Opening single Hibernate Session in HibernateLazyResolver");
session = getSession(sessionFactory);
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
}
}
else {
// deferred close mode
if (SessionFactoryUtils.isDeferredCloseActive(sessionFactory)) {
// Do not modify deferred close: just set the participate flag.
participate = true;
}
else {
SessionFactoryUtils.initDeferredClose(sessionFactory);
}
}
}
/**
* 释放 session, 在 lazy 的结束处调用
*
*/
public void releaseSession() {
if (!participate) {
if (isSingleSession()) {
// single session mode
TransactionSynchronizationManager.unbindResource(sessionFactory);
logger.debug("Closing single Hibernate Session in HibernateLazyResolver");
try {
closeSession(session, sessionFactory);
}
catch (RuntimeException ex) {
logger.error("Unexpected exception on closing Hibernate Session", ex);
}
}
else {
// deferred close mode
SessionFactoryUtils.processDeferredClose(sessionFactory);
}
}
}
/**
* Get a Session for the SessionFactory that this filter uses.
* Note that this just applies in single session mode!
* <p>The default implementation delegates to SessionFactoryUtils'
* getSession method and sets the Session's flushMode to NEVER.
* <p>Can be overridden in subclasses for creating a Session with a custom
* entity interceptor or JDBC exception translator.
* @param sessionFactory the SessionFactory that this filter uses
* @return the Session to use
* @throws DataAccessResourceFailureException if the Session could not be created
* @see org.springframework.orm.hibernate.SessionFactoryUtils#getSession(SessionFactory, boolean)
* @see org.hibernate.FlushMode#NEVER
*/
protected Session getSession(SessionFactory sessionFactory) throws DataAccessResourceFailureException {
Session session = SessionFactoryUtils.getSession(sessionFactory, true);
// 注意这里与 OpenSessionInViewInterceptor 不同, 需要设置为 auto, 否则会导致以下异常
// org.springframework.dao.InvalidDataAccessApiUsageException:
// Write operations are not allowed in read-only mode (FlushMode.NEVER) -
// turn your Session into FlushMode.AUTO or remove 'readOnly' marker from transaction definition
session.setFlushMode(FlushMode.AUTO);
return session;
}
/**
* Close the given Session.
* Note that this just applies in single session mode!
* <p>The default implementation delegates to SessionFactoryUtils'
* releaseSession method.
* <p>Can be overridden in subclasses, e.g. for flushing the Session before
* closing it. See class-level javadoc for a discussion of flush handling.
* Note that you should also override getSession accordingly, to set
* the flush mode to something else than NEVER.
* @param session the Session used for filtering
* @param sessionFactory the SessionFactory that this filter uses
*/
protected void closeSession(Session session, SessionFactory sessionFactory) {
session.flush();
SessionFactoryUtils.releaseSession(session, sessionFactory);
}
}
使用方法, 在配置文件中声明
代码
[html] view plaincopy
<!-- use to resolve hibernate lazy load -->
<bean id="hibernateLazyResolver" class="org.summerfragrance.support.hibernate3.HibernateLazyResolver">
<property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>
<bean id="userManager" parent="txProxyTemplate">
<property name="target">
<bean class="org.summerfragrance.security.service.impl.UserManagerImpl" parent="managerTarget">
<property name="userDAO"><ref bean="userDAO"/></property>
<property name="hibernateLazyResolver"><ref bean="hibernateLazyResolver"/></property>
</bean>
</property>
</bean>
然后在代码中这样调用
[java] view plaincopy
hibernateLazyResolver.openSession();
....
//需要 lazy load 的代码
hibernateLazyResolver.releaseSession();
如果是 TestCase, 可以简单的设置 BaseTestCase 如下
代码
package org.summerfragrance;
import junit.framework.TestCase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.summerfragrance.support.hibernate3.HibernateLazyResolver;
/**
* Base class for running DAO tests.
*
* @author mraible
*/
public class BaseTestCase extends TestCase {
protected final Log log = LogFactory.getLog(getClass());
protected final static ApplicationContext ctx;
protected HibernateLazyResolver hibernateLazyResolver;
static {
String[] paths = { "/conf/applicationContext-dataSource.xml",
"/org/summerfragrance/vfs/applicationContext-vfs.xml",
"/org/summerfragrance/security/dao/hibernate/applicationContext-hibernate.xml"
// "/org/summerfragrance/security/dao/jdbc/applicationContext-jdbc.xml"
};
ctx = new ClassPathXmlApplicationContext(paths);
}
/**
* @see junit.framework.TestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
hibernateLazyResolver = (HibernateLazyResolver) ctx
.getBean("hibernateLazyResolver");
hibernateLazyResolver.openSession();
}
/**
* @see junit.framework.TestCase#tearDown()
*/
protected void tearDown() throws Exception {
super.tearDown();
hibernateLazyResolver.releaseSession();
hibernateLazyResolver = null;
}
}
这样就可以在 Service 和 TestCase 中使用 Lazy Load 了, 目前已经测试通过
这几天看 JavaEye 上关于 OpenSessionInView 的讨论, 感觉这个问题比较常见
代码
在代码中调用 openSession(), 然后不予处理, 这就是 ajoo 说的第一种不擦屁股就直接走人的做法, 这样可能导致两种错误;
a. org.springframework.orm.hibernate3.HibernateSystemException: Illegal attempt to associate a collection with two open sessions
b. 数据库连接不关闭
正确的做法是用 HibernateCallBack 或者参照 HibernateTemplate 对 session 进行处理
以上都是一些个人想法, 小弟学习 Hibernate 不久, 欢迎各位拍转
代码