论坛首页 Java企业应用论坛

用 OpenSessionInViewInterceptor 的思路解决 Hibernate Laz...

浏览 33872 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2005-07-14  
众所周知, 为了解决 Hibernate Lazy 问题, Spring 中引入了 OpenSessionInViewInterceptor, 这样虽然解决了页面上的 Lazy Load 问题,却增加了各层之间的偶合性,
如果一个 Lazy 的 Collection 在页面上可以被正确的 load, 但是如果请求不是来自于 HttpServletRequest (比如在 TestCase 或 Service 中希望获取 lazy 的属性),
一般会导致两种错误:
1. 设置了 lazy = "true"
   会导致 org.hibernate.LazyInitializationException: failed to lazily initialize a collection of xxx: xxx - no session or session was closed
2. 设置里 lazy = "false"
   会导致 org.hibernate.LazyInitializationException: could not initialize proxy - the owning Session was closed


为了方便测试, 灵活使用 lazy load, 我按照 OpenSessionInViewInterceptor 的思路实现了一个 HibernateLazyResolber, 代码如下:


/*
 * Copyright 2004-2005 wangz.
 * Project shufe_newsroom
 */
package org.summerfragrance.support.hibernate3;

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.hibernate3.support.OpenSessionInViewInterceptor
 * @see org.springframework.orm.hibernate3.support.OpenSessionInViewFilter
 * @since 2005-7-14
 * @author 王政
 * @version $Id: HibernateLazyResolver.java,v 1.4 2005/07/14 14:15:19 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.hibernate3.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); {
	    // 需要 flush session
	    session.flush();;
		SessionFactoryUtils.releaseSession(session, sessionFactory);;
	}
}


使用方法, 在配置文件中声明


	<!-- 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>



然后在代码中这样调用

        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 不久, 欢迎各位拍转
   发表时间:2005-07-14  
实际项目中往往需要的是Session的Scope跨越多个类方法调用,甚至跨越Web层页面的render。你这种方法需要显式在代码中调用语句来完成Session的打开和关闭,不要说满足不了页面的render,连跨越多个类方法调用的需求都无法满足。
0 请登录后投票
   发表时间:2005-07-14  
robbin 老大这么快就来拍转,  真是惭愧亚  , 不过我觉得这种方法至少可以在某种程度上解决一些问题,  就像我说的 TestCase 和 Service 方法中, 如果需要用到 lazy, 那应该怎么解决呢? 今天也是被郁闷了一天才想到这个办法
0 请登录后投票
   发表时间:2005-07-14  
package com.iteye.common.test;

import junit.framework.TestCase;

import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public abstract class AbstractTestBean extends TestCase {

	protected ApplicationContext applicationContext;

	private SessionFactory sessionFactory;

	private Session session;

	protected void setUp() throws Exception {

		String configFile = "spring/*.xml";
		applicationContext = new ClassPathXmlApplicationContext(configFile);
		sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory");
		session = SessionFactoryUtils.getSession(sessionFactory, true);
		session.setFlushMode(FlushMode.NEVER);
		TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
	}

	protected void tearDown() throws Exception {
		TransactionSynchronizationManager.unbindResource(sessionFactory);
		SessionFactoryUtils.releaseSession(session, sessionFactory);
	}
}


这是我的TestCase的基类,所有的TestCase继承这个类,就可以具备OpenSessionInView的功能。
1 请登录后投票
   发表时间:2005-07-15  
偶的做法和Robbin几乎是一样的,不过有时候还是需要测试多个session的时候,所以多加了一个newSession()的方法。
public abstract class AbstractTest extends TestCase {
    protected ApplicationContext context;

    protected void setUp(); throws Exception {
        context = getContext();;
        openSession();;
        super.setUp();;
    }

    protected abstract ApplicationContext getContext();;

    private void openSession(); {
        SessionFactory sessionFactory = (SessionFactory); context.getBean("sessionFactory");;
        Session hibSession = SessionFactoryUtils.getSession(sessionFactory, true);;
        hibSession.setFlushMode(FlushMode.NEVER);;
        TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(hibSession););;
    }
    
    protected void newSession(); {
        closeSession();;
        openSession();;
    }    

    protected void tearDown(); throws Exception {
        super.tearDown();;
        closeSession();;
    }

    private void closeSession(); {
        SessionFactory sessionFactory = (SessionFactory); context.getBean("sessionFactory");;
        SessionHolder sessionHolder = (SessionHolder); TransactionSynchronizationManager.unbindResource(sessionFactory);;
        SessionFactoryUtils.closeSessionIfNecessary(sessionHolder.getSession();, sessionFactory);;
    }
}


如果有需要用到多个session的时候:
public void testAbc(); {
    context.getBean("service");.foo();;
    newSession();;
    context.getBean("service");.bar();;
}


其实这种可以称为OpenSessionInClient,不管View还是Test Code的Client,只要在同一个JVM里面,就可以用OpenSession的方式处理掉。如果Hibernate的实体对象丢到另外一个JVM中,还想拥有LazyLoad便捷性,就麻烦多了,Robbin有何好的方法?
2 请登录后投票
   发表时间:2005-07-15  
Spring的AbstractTransactionalDataSourceSpringContextTests里的实现也可以参考。
0 请登录后投票
   发表时间:2005-07-15  
引用
其实这种可以称为OpenSessionInClient,不管View还是Test Code的Client,只要在同一个JVM里面,就可以用OpenSession的方式处理掉。如果Hibernate的实体对象丢到另外一个JVM 中,还想拥有LazyLoad便捷性,就麻烦多了,Robbin有何好的方法?


记得这个问题我们和jackz前面曾经讨论过,你还尝试使用Hibernate3某新特性去解决这类问题。我的答案是目前没有好的解决办法。

Hibernate能对实体对象lazy loading的一个前提是对实体对象动态增强,使用了该实体对象的Proxy,而这个Proxy代理了实体对象的getter/setter的方法,在proxy自己的getter/setter方法里面增加数据库查询操作(我没有看过这部分源代码,只是推测)。

根据这个原理,要想实现跨JVM的lazy loading,即分布式调用,那么实体对象的远程Proxy类,必须具备可以定位服务器地址,发送远程方法调用的能力,这将要求如下条件:

1、持有该实体类的客户端必须有Hibernate的Session环境,实体类必须在一个Session的scope范围之内load,来支持生成实体类的远程Proxy。

2、服务器端的实体类的Proxy类必须具备远程方法调用的能力,即可以作为服务器的一项服务被发布出来,可以被客户端查找并且调用。

其实传统的CMP就是这样的模型,满足上面所述的条件,可以支持跨JVM的lazy loading。所以我在观望EJB3 EntityBean,看看它出来以后,是否还具备这样的能力。

不过这种分布式lazy loading的需求往往客户端和服务器端环境是不一样的,很可能是服务器端是Java,而客户端环境是AJAX,或者Flash ,这样的话,EJB3也无用武之地了。

在RIA模型中,其实现在已经遇到这种麻烦了。当前的RIA模型,以Flash AMF为例,服务器端有一个Flash Gateway,其实就是一个Servlet,这个Servlet接受Flash发送过来的消息(Flash使用AMF协议,类似Hessian/Burlap,但是协议更完整),然后根据消息,去调用相应的服务器端组件(例如现在可以直接按照bean的id去调用相应的Spring Bean),然后把结果封装为AMF消息返回。

例如一个Hibernate实体对象通过AMF调用序列化到Flash里面了,即成为Flash里面的一个ActionScript Object。这个AS的Object是无法lazy loading的。在默认情况下,AMF Gateway在把实体对象序列化到客户端的时候,其序列化过程会遍历整个对象图,将整张对象图全部序列化到客户端。

针对这个问题,我当时的做法就是修改OpenAMF的序列化源代码,当遇到one-to-many的关联,即打断关联关系,不序列化关联集合,而many-to-one和one-to-one关联则序列化关联类。

客户端的Flash AS Object如果需要关联集合,则需要使用这个对象作为消息,再次显式发送AMF调用,获取该实体对象的关联集合。

这种方式下,由于打断one-to-many关联,所以不再支持inverse="false",即父对象维护关联,所以要求所有的one-to-many都改为双向关联,关联关系由子对象一方维护。

这样你就可以在Flash里面,先通过AMF调用,获得一个父对象,然后再调用一次获得这个对象的集合属性,然后可以进行各种关联关系的维护操作,最后AMF调用子对象的update操作,即可。

这是我目前的折衷的做法,我觉得还可以接受,这种方式下,仅仅要求你在客户端多发送一次显式查询来loading集合属性,其它方面和在服务器端直接操作没有多大区别。
0 请登录后投票
   发表时间:2005-08-18  
如果session的关闭由域模型来控制的话...lazyload的似乎更好理解了


因为client想使用的时候域模型(自己带有lazy load),并不关心manager是什么,所以session放在manager层管理,就会出现lazyload的尴尬问题..如果由域模型自己管理,又会出现多session问题(manager被多次调用.返回多个域模型)

个人拙见...
2 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics