Richie

Sometimes at night when I look up at the stars, and see the whole sky just laid out there, don't you think I ain't remembering it all. I still got dreams like anybody else, and ever so often, I am thinking about how things might of been. And then, all of a sudden, I'm forty, fifty, sixty years old, you know?

NHibernate考察系列 06 进阶篇

    1. 实体生命周期
    Transient: 临时状态。代码中使用new创建一个实体的对象实例时,其状态为transient。
    Persistent: 持久化状态。实体的对象实例跟数据库中一条记录对应,并且其session处于活动状态。例如使用session.Get()方法加载的实体对象、使用session.Save()保存过的实体对象,在session关闭以前其状态为persistent。Transient状态的对象调用session.Save()、session.Update()等方法,并且session未关闭,其状态变成persistent。
    Detached: 游离状态。实体的对象实例跟数据库中一条记录对应,并且其session处于非活动状态。Persistent状态的对象,在对应的session关闭之后,其状态变成detaiched。Detached对象可以重新跟某个session建立联系,被这个session管理起来,例如对detached对象调用session.Update()。

    Persistent状态的对象是受管理的对象,它被放置在当前session的管理容器中,对其属性的修改会被记录,当session的事务提交时,所有这些更新修改会被提交到数据库。Transient和detached状态的对象不受管理。
    Transient和detached状态的区别是,对detached状态的对象,能够确定它跟数据库中的一条记录对应,而transient状态则不一定。NHibernate有很多的实体id生成方法,不少情况下transient状态的对象id属性没有值,只有在执行session.Save()过程中才会生成id值。Detached对象的各个属性都是完整的(跟数据库中的记录对应),包括id属性值。如果对象的id设置成由程序代码指定的方式,则你在代码中new一个对象,通过给该对象各个属性设置值,也可以使它成为detached状态的对象。

    2. Session级别缓存(一级缓存)、SessionFactory级别缓存(二级缓存)
    一级缓存处于session级别,只能够被当前的session使用,当你的session关闭之后,这些缓存被清除掉。你对一级缓存不能进行多少控制,例如最大缓存多少个对象等。二级缓存在SessionFactory级别,这意味着从这个SessionFactory创建的所有session对象能共享这个缓存。二级缓存允许更多的控制,例如设定一个最大缓存的对象数量,以防止缓存使用过多的内存;针对每个实体可以配置缓存策略。
    NHibernate提供很多方式操纵实体和数据库,这给缓存与数据库之间的一致性带来问题,你需要了解这些问题,在程序面避免这种一致性问题发生,或者保证它是在你的控制之内。举一个例子,假如你通过session.Get()方法获取了一个对象,这时在二级缓存中该对象被缓存,然后在另外的地方有人使用Native SQL在数据库中把这条记录删除了。这种情况下,二级缓存中该对象仍然是有效的,别人在使用session.Get()获取这个对象时NHibernate会返回二级缓存中的对象,但是在数据库中这个对象已经不存在了!

    另外是关于批量操作的问题。
    首先是批量插入、更新。一级缓存没有最大容量限制,所有persistent状态的对象都被缓存在一级缓存中,修改更新被记录下来,只在commit transaction的时候,才将更新提交数据库,在session关闭的时候清除一级缓存。假如你一次批量插入、更新10万、100万的对象,过大的缓存会导致大量的内存占用、低下的性能表现。可以使用下面的措施做一些改善:例如每500个对象执行一次应用到数据库和清除缓存的操作:
session.Flush();   //将缓存中的变化应用到数据库
session.Clear();   //清除当前session的缓存
    使用batch-size使NHibernate一次提交多个SQL语句,在性能上会有一定的改善。方法是在批量操作的对象映射配置中将batch-size设置为50、100等。
    其次是关于批量删除操作。如果你使用
int deletedRows = session.Delete("from PlantItem");
    NHibernate用下面的方式实现
IList<PlantItem> list = session.CreateQuery("from PlantItem").List<PlantItem>();
for (int i = 0; i < list.Count; i++)
{
    session.Delete(list[i]);
}
    这是因为NHibernate需要保证缓存跟数据库的一致性,上面这样的处理是跟缓存机制结合在一起的,保证了一致性(目前HB只能通过这种方式来保证),但却带来性能的损失。
    假如你一次delete的数据有几百万,就不能这样来实现。一种方式,类似上面批量插入、更新的优化手段一样来处理,你需要进一步改善一些,例如每次用session加载1000、5000的对象(避免一次读个几百万),循环的处理。另一种方式就是使用Native SQL等,优点是速度快,但跨过了NHB的缓存机制,会导致缓存不一致。
    可以使用SessionFactory的几个方法手工清除二级缓存,可以清除某个对象实例、整个类的二级缓存对象,也可以清除collection、query的二级缓存。例如:
sessionFactory.Evict(typeof(PlantItem));  //清除二级缓存中所有PlantItem对象

    启用二级缓存的方法。首先在NHibernate的configuration里面添加缓存配置属性
<property name="cache.provider_class">
      NHibernate.Caches.Prevalence.PrevalenceCacheProvider, NHibernate.Caches.Prevalence

</
property>
<property name="cache.use_query_cache">false</property>
<property name="expiration">120</property>
    然后为每个实体类选择缓存策略
<class name="PlantItem" table="TBLPLANTITEM" batch-size="10">
  
<cache usage="read-write" />
    Cache provider和缓存策略参考文档进行选择。use_query_cache选项谨慎使用。
    use_query_cache启用方法:在配置节点中将cache.use_query_cache配置为true,程序代码中,使用IQuery、ICriteria的地方调用IQuery.SetCacheable(true)或者ICriteria.SetCacheable(true),则对这些查询会启用缓存。
    如果使用NHibernate.Caches.Prevalence,可以在NHibernate的configuration里面添加一个配置项,用于设置Bamboo.Prevalence的缓存目录,如果不设置缓存目录,默认是当前应用程序目录。配置示例如下:
<property name="prevalenceBase">
      D:\Work\CMSDesign\PreTest\CMS\Web\CacheFolder
</property>
     看了下NHibernate.Caches.Prevalence源代码,请特别注意一点,NHibernate在停止NHibernate.Caches.Prevalence缓存的时候,会把这个缓存目录删除掉,因此使用NHibernate.Caches.Prevalence作为缓存时,一定要妥善的设置好缓存目录!另外,如果你用VS2005建立的是Web Site Project(Web Application Project的情况我不大清楚),调试时使用的是WebDev.WebServer.exe,在Stop的时候并不会触发Application_End事件,因此你没有机会执行ISessionFactory.Close(),NHibernate.Caches.Prevalence的缓存目录不会被清除掉,因此下一次再执行调试而启用缓存的时候,会导致异常。有两个解决方法,一是每次手工删除缓存目录,另外一种是写一个类似exit.aspx页面,在每次退出时手工执行一次ISessionFactory.Close(),这样就会将缓存清除。

    如果使用NHibernate.Caches.SysCache,就可以象ASP.Net一样配置缓存策略,例如缓存过期时间等。还可以建立不同的Region,使用不同的缓存策略,然后在Class映射文件的<cache usage="" region="" />中通过指定region,而使用不同的缓存策略配置。
<configuration>
    
<configSections>
        
<section name="syscache"
                type
="NHibernate.Caches.SysCache.SysCacheSectionHandler,NHibernate.Caches.SysCache" />
    
</configSections>
    
<syscache>
        
<cache region="foo" expiration="500" priority="4" />
        
<cache region="bar" expiration="300" priority="3" />
    
</syscache>
</configuration>

    3. 拦截器
    实现一个IInterceptor类,使用下面语句为创建的session添加一个拦截器
session = sessionFactory.OpenSession(new MyInterceptor());
    或者用下面的方法为SessionFactory设置一个全局拦截器
new Configuration().SetInterceptor(new MyInterceptor()).Configure().BuildSessionFactory();
    下面先给出一个实现IInterceptor的模板,然后比较详细的讲述一下拦截器中的主要方法。
public class MyInterceptor : IInterceptor
{
    
public void AfterTransactionBegin(ITransaction tx)
    {
    }

    
public void AfterTransactionCompletion(ITransaction tx)
    {
    }

    
public void BeforeTransactionCompletion(ITransaction tx)
    {
    }

    
public int[] FindDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames
                                  , IType[] types)
    {
        
return null;
    }

    
public object Instantiate(System.Type type, object id)
    {
        
return null;
    }

    
public object IsUnsaved(object entity)
    {
        
return null;
    }

    
public void OnDelete(object entity, object id, object[] state, string[] propertyNames, IType[] types)
    {
    }

    
public bool OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames
                                      , IType[] types)
    {
        
return false;
    }

    
public bool OnLoad(object entity, object id, object[] state, string[] propertyNames, IType[] types)
    {
        
return false;
    }

    
public bool OnSave(object entity, object id, object[] state, string[] propertyNames, IType[] types)
    {
        
return false;
    }

    
public void PostFlush(ICollection entities)
    {
    }

    
public void PreFlush(ICollection entities)
    {
    }
}

    a) Instantiate、OnLoad:
    在调用session.Get()、session.Load()等方法时,NHibernate先执行SQL语句获取数据库记录,如果数据库返回了记录,则说明指定的id实体对象在数据库中存在,接下来NHibernate就要创建这个实体对象并返回。在创建实体对象之前,先执行拦截器的
Instantiate方法,之后NHibernate创建这个实体对象,调用拦截器的OnLoad方法,OnLoad返回之后,NHibernate再为实体对象设置各个属性值。
    OnLoad方法主要参数,
object entity是创建的实体对象,除了ID属性以外,其它属性都还没有赋值。object[] state是实体各个属性的值,将要被赋给object entity的各个属性。string[] propertyNames和IType[] types是实体的各个属性名称以及IType列表。
    b) OnDelete:
    在调用session.Delete()方法时会执行拦截器的OnDelete方法。注意session.Delete()方法调用时并不会执行数据库操作的SQL,NHibernate只是将session管理中的对象做一些状态等方面的处理。
    参数说明同a)。
    c) OnSave:
    对transient状态的对象调用session.Save()方法时会执行拦截器的OnSave方法。注意session.Save()方法
调用时并不会执行数据库操作的SQL,另外对persistent状态的对象调用session.Save()并不会执行拦截器OnSave方法。
    参数说明同a)。
    d) PreFlush、PostFlush:
    在调用session.Flush()或者transaction.Commit()方法时会执行拦截器的这两个方法。
session.Flush()的作用是要将当前session缓存(一级缓存)中受管理对象的新增、修改、删除等操作应用到数据库,所以它需要遍历session缓存中的每一个受管理的对象,以确定需要进行的处理。PreFlush是开始执行Flush操作之前的拦截器方法,PostFlush是执行完Flush操作之后的拦截器方法。
   
ICollection entities参数是session缓存中受管理的对象(即需要遍历的对象)。PreFlush参数的entities包括当前session中全部受管理的对象,而PostFlush中,不包括那些调用session.Delete()删除的对象。
    e)
FindDirty、OnFlushDirty:
    Flush操作需要遍历缓存中的对象,检查每一个对象的每一个属性值是否有被修改过(Is dirty? 这个动作对应的拦截器方法为FindDirty),对有修改过的对象,将生成数据库的UPDATE SQL
操作(对应的拦截器方法为OnFlushDirty)。调用session.Save()插入(指新增,而不是更新)的对象、调用session.Delete()删除的对象,不会执行OnFlushDirty方法,OnFlushDirty只针对那些需要生成数据库UPDATE操作的对象。
    对应于PreFlush入参entities中的每一个对象,都会调用一次FindDirty拦截器方法,只有对那些属性值有做过修改的对象(is  dirty==true),才会调用OnFlushDirty拦截器方法。
    入参
object entity是当前正在遍历的对象。object[] currentState是当前对象各个属性的值。object[] previousState是对象刚从数据库加载出来时各个属性的值。NHibernate通过比较currentState和previousState中对应的属性值是否一致来判断该对象是否应当被UPDATE。
    下面说说这两个方法的区别。

    FindDirty方法为NHibernate使用者提供一个机会,可以自己检查对象是否为dirty,NHibernate以此确定是否需要更新这个对象。返回值是一个整数数组,指示哪些属性是dirty的,例如如果某个对象的第一个和第五个属性为dirty,则返回的数组为int result[] = {0,4}。如果返回值为null,NHibernate还是会用自己的方法再检查一遍,以确认对象是否为dirty的。只要你的返回值不是null,NHibernate就认为这个对象需要更新;而一旦返回null,NHibernate就用自己的方法来检查是否需要更新。所以对于某个特定的,有修改过的对象,如果你要在拦截器里面阻止NHibernate进行更新,好像是没有什么办法的。一个未经测试的方法是,第一在
FindDirty方法中返回null,第二将FindDirty的currentState和previousState设成一样,首先这个方法没有测试是否行的通,其次这样做会不会带来其它问题没有研究了,仅仅只是一个设想,不要轻易采用。
   
OnFlushDirty是NHibernate确定了一个对象是dirty的,需要更新,在拦截器中提供这个方法,是在更新对象之前,给使用者提供一个修改对象的机会。

posted on 2007-04-17 01:18 riccc 阅读(...) 评论(...) 编辑 收藏

导航

News