一次OSIV调试经历

使用nHibernate有一段时间了,但一直没有使用它的延迟加载机制,究其根本大概源于刚接触nHibernate的失败调试经历——因为总是看到诸如“Could not initialize proxy - the owning Session was closed.”或者线程没有绑定Session之类的异常:

实际上园子里提供了不少解决这类问题的方案,如李永京的 NHibernate之旅(13):初探立即加载机制 等, 但终归觉得不够理想——较之各种方案描述的单一环境,既有框架是构建在spring.net之上的,意味着对待Session不进行侵入式管理,更进一步,spring.net对于Session的请求响应要覆盖到所有View。

直到最近,重新拜读了 NHibernate之旅(9):探索父子关系(一对多关系) ,意识到关联对象的加载是个问题,决定还是得把延迟加载用起来,事关性能。

经 刘冬 先生的指点,知道了spring.net有一种管理Session以用于lazy-load的模式——Open Session In View(OSIV),原理大致是

spring.net提供了一个HttpModule,“就是一个filter,每次request进来,就打开一个session放到ThreadLocal里,以后用到session就拿出来用,filter结束的时候,再清空ThreadLocal,关闭session”

 OSIV模式一旦开启,lazy-load所需Session的绑定生灭,便不再需要开发者操心,听着确实很爽,于是便开始了长达一周噩梦般的调试:

 一、先更新一下spring.netnHibernate版本,分别是1.3.0RC和2.1.1GA。

 二、更新各层引用 编译报错: 

错误 34 “NHibernate.Engine.ISessionFactoryImplementor”不包含“CloseConnection”的定义,并且找不到可接受类型为“NHibernate.Engine.ISessionFactoryImplementor”的第一个参数的扩展方法“CloseConnection”(是否缺少 using 指令或程序集引用?) ...\DaoTemplate.cs 833 21 woodigg.DAO

在nH 2.1中,ISessionFactoryImplementor接口不再包含CloseConnection方法,将其注释,并查找了一下DaoTemplate中所有关于Session.Close的代码,全部注释(在OSIV中,不再干预Session的生命周期)。编译通过。 

三、修改配置文件中的HibernateSessionFactory对象,在HibernateProperties中添加这样一个条目:

appDaoConfig.xml
<!--动态代理-->
<entry key="proxyfactory.factory_class"
                    value
="NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle" />

 同时引用nHibernate.Bytecode.Castle和Castle.DynamicProxy2程序集。

四、修改web.config,注册OSIV模块,并让其作用于HibernateSessionFactory对象: 

web.config
<appSettings>
    
<!--SessionFactory-->
    
<add key="Spring.Data.NHibernate.Support.OpenSessionInViewModule.SessionFactoryObjectName" value="HibernateSessionFactory"/>
</appSettings>

<httpModules>
    
<!--OpenSessionInView-->
    
<add name="OpenSessionInView" type="Spring.Data.NHibernate.Support.OpenSessionInViewModule, Spring.Data.NHibernate21"/>
</httpModules>

 五、在Mvc2站点中添加一个View,取一个拥有关联集合子对象的数据列表(如下图,ARTIST对象有一个ALBUMS关联对象,它是ALBUM对象的集合,一对多):

 此时使用的搜索代码来自DaoTemplate,依赖于spring.net提供的HibernateTemplate

DaoTemplate.cs
IList alist = HibernateTemplate.Find(hql); 

六、使用MvcContrib的Grid进行绑定: 

Index.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<ARTIST>>" %>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    
<fieldset>
        
<legend>List</legend>
        
<%= Html.Grid(Model).Columns(
            column =>{
                    
column.For(c => c.Id).Named("ID");
                    column.For(c => c.Name).Named("名儿");
                    column.For(c => c.Belong).Named("在哪儿");
                    column.For(c => c.ALBUMS.Count).Named("专辑数");
                })
            .Attributes(style => "90%")
            .RowStart(c => string.Format("<tr class='{0}'>", c.IsAlternate ? "alt" : ""))
            
%>
    
</fieldset>
</asp:Content>

七、运行,出现异常“Could not initialize proxy - no Session.

八、查看Log4net日志,有这么一段:

2009-11-20 10:03:44,343 [1] DEBUG NHibernate.Transaction.AdoTransaction [(null)] - IDbTransaction disposed.
2009-11-20 10:03:44,343 [1] DEBUG NHibernate.Impl.SessionImpl [(null)] - closing session
2009-11-20 10:03:44,343 [1] DEBUG NHibernate.AdoNet.AbstractBatcher [(null)] - running BatcherImpl.Dispose(true)
2009-11-20 10:03:47,140 [1] ERROR NHibernate.LazyInitializationException [(null)] - Initializing[Ic.Model.ALBUM#1]-Could not initialize proxy - no Session.

这说明一个问题:HibernateTemplate把Session关闭了

九、弃用HibernateTemplate,写一个抽象类,把SessionFactory植入后,只负责调用活动的Session:

HibernateDao.cs
using NHibernate;

namespace woodigg.DAO
{
    
/// <summary>
    
/// Base class for data access operations.
    
/// </summary>
    public abstract class c    {
        
private ISessionFactory sessionFactory;

        
/// <summary>
        
/// Session factory for sub-classes.
        
/// </summary>
        public ISessionFactory SessionFactory
        {
            
protected get { return sessionFactory; }
            
set { sessionFactory = value; }
        }

        
/// <summary>
        
/// Get's the current active session. Will retrieve session as managed by the 
        
/// Open Session In View module if enabled.
        
/// </summary>
        protected ISession CurrentSession
        {
            
get
            {
                
return sessionFactory.GetCurrentSession();
            }
        }
    }
}

十、将DaoTemplate的父类由HibernateDaoSupport改为HibernateDao,配置关系不用变化:

appDaoConfig.xml
<object id="DaoTemplate" type="woodigg.DAO.DaoTemplate, woodigg.DAO">
   
<property name="SessionFactory" ref="HibernateSessionFactory" />
</object>

十一、再次运行,这次抛出的异常是(图1):

“No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here. ”

异常来自于sessionFactory.GetCurrentSession()方法,它无法为当前请求提供Session。这意味着:OSIV在MVC中没有生效。

 十二、从此时起,走上了弯路:

  1. 以为OSIV需要提供一个初始化的Session供其调用,遂将CurrentSession做成了单例,但其后发生了“access the same ISession in two concurrent thread” 这样的错误;
  2.  依旧使用sessionFactory.GetCurrentSession(),使用Spring.Transaction.Interceptor.TransactionInterceptor 将 事务管理器Spring.Data.NHibernate.HibernateTransactionManager 引入到 DaoTemplate中,这样做确实见效了,运行伊始就有一个Session,但lazy-load失败了,所有关联对象均无法加载,在运行时监视栏里查看它们,都形如“proxy223355124...”。 很明显,Session完成任务就撤了!

十三、冷静下来想想,其实从始至终,OSIV都没有生效过。那究竟是什么导致的呢?会不会是OpenSessionInViewModule没有被spring.net引入到view中?

所有的线索,指向了那个一直被信任的控制器工厂SpringControllerFactory ,看看它是怎么实现的:

SpringControllerFactory.cs
public class SpringControllerFactory : IControllerFactory
{
//..
        private static DefaultControllerFactory defalutf = null;
        
public IController CreateController(RequestContext context, string name)
        
{
            IApplicationContext configContext
                
= new XmlApplicationContext(ParameterFactory.CfgFilePath,
                    ParameterFactory.CfgBusinessFilePath, ParameterFactory.CfgControllersFilePath,
                    ParameterFactory.CfgServicesFilePath);
            
string controllName = GetFirstUpcaseName(name) + "Controller";           
            
if (configContext.ContainsObject(controllName))
            
{
                
return (IController)configContext.GetObject(controllName);
            }

            
else
            
{
                
if (defalutf == null)
                
{
                    defalutf 
= new DefaultControllerFactory();
                }

                
return defalutf.CreateController(context, name);
            }

        }

//..
}

 杯具啊!这个一年前的产物,在使用IApplication这个问题上手法笨拙,依靠预定义的配置文件路径引入环境。也许在CS结构中会有用处,但在BS环境中,这样更直接更全面(文件引用,实际没把包含OSIV配置的web.config引入环境,view扑空):

SpringControllerFactory.cs
WebApplicationContext configContext =
                ContextRegistry.GetContext() 
as WebApplicationContext;

十四、至此,OSIV成功覆盖整个mvc应用。凡事有利弊,在java社区会有这样的讨论:

open session in view 在访问量很大的时候,容易造成页面假死现象

“因为osiv会在每次请求的过程中占用一个session,如果这个请求过程太长,session就无法释放了。可以在tomcat前端加一个apache,讲网速太慢的请求隔离掉。”

 也许在这样的时候,一个好的网络运维,比你吭哧更管用 :)

Creative Commons License
本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 Unported许可协议
欢迎转载,但必须保留文章的署名老莫的帐本子
并保留此链接:http://moye.cnblogs.com/
如有疑问请发邮件:moyerock@gmail.com
原文地址:https://www.cnblogs.com/moye/p/1611907.html