论坛首页 Java版 Spring

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

浏览 15823 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
最后更新时间:2005-07-14
众所周知, 为了解决 Hibernate Lazy 问题, Spring 中引入了 OpenSessionInViewInterceptor, 这样虽然解决了页面上的 Lazy Load 问题,却增加了各层之间的偶合性,
如果一个 Lazy 的 Collection 在页面上可以被正确的 load, 但是如果请求不是来自于 HttpServletRequest (比如在 TestCase 或 Service 中希望获取 lazy 的属性),
一般会导致两种错误:
[code:1]
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
[/code:1]

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

[code:1]

/*
* 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);
}
}
[/code:1]

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

[code:1]

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

[/code:1]

然后在代码中这样调用

[code:1]
        hibernateLazyResolver.openSession();
       
        ...
        //需要 lazy load 的代码
       
        hibernateLazyResolver.releaseSession();
[/code:1]

如果是 TestCase, 可以简单的设置 BaseTestCase 如下
[code:1]

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;
    }

}

[/code:1]

这样就可以在 Service 和 TestCase 中使用 Lazy Load 了, 目前已经测试通过

这几天看 JavaEye 上关于 OpenSessionInView 的讨论, 感觉这个问题比较常见

[code:1]

  在代码中调用 openSession(), 然后不予处理, 这就是 ajoo 说的第一种不擦屁股就直接走人的做法, 这样可能导致两种错误;
   a. org.springframework.orm.hibernate3.HibernateSystemException: Illegal attempt to associate a collection with two open sessions
   b. 数据库连接不关闭
   正确的做法是用 HibernateCallBack 或者参照 HibernateTemplate 对 session 进行处理

[/code:1]

以上都是一些个人想法, 小弟学习 Hibernate 不久, 欢迎各位拍转
   
最后更新时间:2005-07-14
实际项目中往往需要的是Session的Scope跨越多个类方法调用,甚至跨越Web层页面的render。你这种方法需要显式在代码中调用语句来完成Session的打开和关闭,不要说满足不了页面的render,连跨越多个类方法调用的需求都无法满足。
   
0 请登录后投票
最后更新时间:2005-07-14
robbin 老大这么快就来拍转,  真是惭愧亚 , 不过我觉得这种方法至少可以在某种程度上解决一些问题,  就像我说的 TestCase 和 Service 方法中, 如果需要用到 lazy, 那应该怎么解决呢? 今天也是被郁闷了一天才想到这个办法
   
0 请登录后投票
最后更新时间:2005-07-14
package com.javaeye.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的功能。
   
0 请登录后投票
最后更新时间:2005-07-15
偶的做法和Robbin几乎是一样的,不过有时候还是需要测试多个session的时候,所以多加了一个newSession()的方法。
[code:1]
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);
    }
}
[/code:1]

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

其实这种可以称为OpenSessionInClient,不管View还是Test Code的Client,只要在同一个JVM里面,就可以用OpenSession的方式处理掉。如果Hibernate的实体对象丢到另外一个JVM中,还想拥有LazyLoad便捷性,就麻烦多了,Robbin有何好的方法?
   
0 请登录后投票
最后更新时间:2005-07-24
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被多次调用.返回多个域模型)

个人拙见...
   
0 请登录后投票
论坛首页 Java版 Spring

跳转论坛:
JavaEye推荐