NHibernate 进阶
作者:Justin Gehtland
(原文:请点击这里)
在我的最近文章中,我介绍了 Nhibernate。在这里和在其他的论坛中 , 因为我没有强调NHibernate 只是许多可得的 ORM 解决方案之一,(事实上,现在对.NET开发者来说,开源的加上商业的 ORM 的架构现在是超过 50个可供选择)。 作为一个开发顾问,我会经常用到Hibernate(大家都知道它吧)既然我必须在.NET平台下选择一个,NHibernate是我最明智的选择。 我想在这里脱离.NET平台来研究一下这个框架,并谈谈我们能从它获得的益处。
我的上一篇文章中介始示范了应用Nhibernate建一个简单应用(大学注册登记)的起步示例。下面我将介绍更多使用Nhibernate的技术,这次我们的目标给你一些在对特别大数据量查询的高吞吐量、高并发性的应用需求下使用Nhibernate的方式。
为了达到这个目标,我将分成下面四部分来讲:最佳的Session管理方式,指导使用HQL查询数据库,怎样延迟载入对象集合及对象生存周期管理。
第一部分:会话管理
在我的前一篇文章中,我实现了一个非常简单的为管理SessionFactory和Sessionr的名为RegMgr的类:它在其构造函数中根据配置创建了一个SessionFactory,然后在每一个方法中使用SessionFactory创建Session,使用这种方式有几个问题。
首先, 每个RegMgr 对象都需要建造一个新的 SessionFactory, 这将耗费非常昂贵资源。 每个SessionFactory都意味对你的应用中的实体与相应的数据库进行一次建模。其实仅仅在你应用需要多个数据库的支持才需创建多个SessionFactory的实例,而我的简单例子不是,所以无需这样。
第二,每个方法从这个Factory创建了一个新的Session。这种方式每当方法完成都会关闭Session并释放锁定的数据库,从而提供了最大程度的并发保护机制。然后这也结束这个会话所有关联对象的状态,其实Session创建也比较耗费资源的,虽然不像SessionFactory那样。
第一个问题的答案是可以肯定的,那就是确认你只创建 SessionFactory 一次。 RegMgr 原来是这样的:
public class RegMgr : DBMgr
{
Configuration config;
ISessionFactory factory;
public RegMgr()
{
try
{
config = new Configuration();
config.AddClass(typeof(nhRegistration.Department));
config.AddClass(typeof(nhRegistration.Person));
config.AddClass(typeof(
nhRegistration.UniversityClass));
factory = config.BuildSessionFactory();
}
catch (Exception ex)
{
// handle exception
}
}
// etc...
}
应修改如下:
public class RegMgr : DBMgr
{
static Configuration config;
static ISessionFactory factory;
static RegMgr()
{
try
{
config = new Configuration();
config.AddClass(typeof(nhRegistration.Department));
config.AddClass(typeof(nhRegistration.Person));
config.AddClass(typeof(
nhRegistration.UniversityClass));
factory = config.BuildSessionFactory();
}
catch (Exception ex)
{
// handle exception
}
}
// etc...
}
这样,所有RegMgr的实例所共享了这同一个SessionFactory,这就节省大量资源。
看起来用同样的方式来解决Session管理的问题也会一样好,那为什么不将一个Static的Session加入到这个类中并仅仅在那个SessionFactory中创建并打开它呢,然后所有的方法都使用同一个Session。答案有两个:首先,共享的Static的Session被多个线程同时访问时可能是线程不安全的,各个事物(Transiction)会相互干扰的;其次,它也大部分时处在空闲状态,但是还是保持一物理数据库连接。而数据库连接是非常珍贵的资源。每一种数据访问方式都有一个相同的目标就是使服务端的游标与一个客户端的连接维持在最短的时间内,Nhibernate也同样如此。因此,无所事事开着的Session相反会是一个很费资源的方式。
替代的解决方案是使用一个非连接态的Session对象:为你所有调用的函数创建一个共享的Session实例或静态对象,在其每个函数的调用开始打开它,离开函数总是关闭它。代码可能是这样的:
public IList getClasses()
{
IList classes = null;
try
{
// session is declared in instance scope
session.Reconnect();
ITransaction tx = session.BeginTransaction();
classes = session.CreateCriteria(
typeof(UniversityClass)).List();
}
catch (Exception ex)
{
// handle exception
}
finally
{
session.Flush();
session.Disconnect();
}
return classes;
}
这样你总能重用同一个Session对象,这样不同方法因调用Session所花费的资源会减到最少,而数据库的资源也都会因及时释放也会使它们协同工作得很好。
第二部分:HQL
我们有时需要NHibernate来处理如:“查询所有部门”或“查询某个学生”之类更特殊更复杂的需求。具体应用中可能需要提供复杂的搜索与数据过滤操作来执行数据查询。NHibernate 提供了与SQL很相似的HQL(the Hibernate Query Language) 。 在 HQL 和 SQL 之间的最大不同点在FROM子句:在HQL中可以使用一个或多个类来表示SQL中的一个或多个表名。既然 NHibernate 的目标就是用Domain objects来提供一个透时的数据层,既然Domain objects与数据表具有对应关系,那么就不再需要提供表名给Nhibernate了。
举例来说,在我的“University registration”程序中,我们可能想知道那些班已安排好但还没有分配任课教师。知道哪一班级已经被预定但是尚未被分配Professors会更好。( 顺便我们可能需要知道其中那些是一年级的班级)。我们为得到它们可以在RegMgr中添加这样一个方法:
public IList getClassesNoProf(){}
然后使用NHibernate来进行完整的实现:
IList classes = this.getClasses();
IList results = new ArrayList();
foreach(UniversityClass c in classes)
{
if(c.Prof == null) results.Add(c);
}
return results;
这里我从数据库获取所有的班级然后遍历它们,并获得最终符合条件的班级。 这样可能会很慢,因为获取了所有对象而最后能需要其中一些甚至很少的一部分,数据表越大这种方式会越慢而难以接受。
所以我们要做的是数据库中直接过滤,这可使数据在我们程序之间的网络带宽需求降至最低(因为数据库通常在网络中的其它服务器上)。
public IList getClassesNoProf()
{
IList results = null;
try
{
ISession session = factory.OpenSession();
results = session.Find(
"from nhRegistration.UniversityClass as
uc where personid is null");
}
catch (Exception ex)
{
// handle exception
}
return results;
实现一个 HQL 查询主要是通过调用 ISession.Find() 方法。 使用它可以执行任意一个查询。 HQL 支持几乎所有SQL子句, 如排序和分组(order by,group by)。为了得到相同的而按Name属性排序的UniversityClass对象集合只需对这个查询做这样的改变就以了:
results = session.Find("from nhRegistration.UniversityClass as
uc where personid is null order by uc.Name asc");
特别注意: 审查排序子句的语法。这里我们按Name来排序,如果你参考我上一篇文章中的映射文件中就有一个名为 “classname”的列。当书写Order子句时,既可以使用属性名也可以数据表的列名,但是如果选择属性你就不得不使用类的别名作为前缀(这个例中就是uc),而使用列名时则不需要(“order by classname asc”)。我宁愿使用类别名加上属性我的方式,因为使我的代码与数据库相分离。
这个HQL查询缺少普通SQL中的SELECT子句,这实际暗示地告诉NHibernate 返回所有From中出现的类型的所有实例。 有时我们并不需要整个对象,比如我们可能仅需要显示课程的名称而无需知道其教师是谁,这时如果载入并初始化这些实例就有些过了,为了克服这个缺点,就可以加入SELECT子句来表示你需要的属性,这样你将得到一个仅包含那些属性的Ilist。在我们的示例中是这样的:
public IList getClassNamesNoProf()
{
IList results = null;
try
{
ISession session = factory.OpenSession();
results = session.Find("select uc.Name from
nhRegistration.UniversityClass as uc
where personid is null order by uc.Name asc");
}
catch (Exception ex)
{
string s = ex.Message;
}
return results;
}
同时,我们也能仅仅得到没有老师的班级数量。
results = session.Find("select count(uc.Name) from
nhRegistration.UniversityClass as uc where personid is null");
正如你所看到的, HQL在大部分地方与SQL相似,除了用面象对象描述来替换数据元素。对于大多数正常的对象持久化工作你完全可以不理会HQL,但是为使更多以数据为中心的工作能完成得更快,善于HQL就会事半功倍。
第三部分:延迟载入(Lazy Loading)
NHibernate一个最棒的地方就是完透明地载入父类的相应子类。你仅仅要求得到父类的时候,而 NHibernate 会去载入所有的子类无论是这种一对一还是一对多的关系。如果这是唯一的选项,那这也是Nhibernate最坏的事情。
这种缺省行为的问题是你的代码对于数据库的使用完全透明而在代码运行时一点也不透明。我的意思因为所有关系被装载对于任何大小的树对象模型来说可能是非常慢的。
现在就以“University registration”的 Domain model为例:这里仅有四个类,但当载入单个Department对象时,就会同时载入一个UniversityClass的集合、Processor的集合。而每一个UniversityClass也会载入一个Processor对象和包括很多Student对象的集合。如果数据库的里有足够多的数据, 仅仅载入一个Department对象已是很笨重了。现在想象一个RegMgr.getAllDepartments()方法,问题就很明显了。
解决这个问题的方法是延迟集合对象的载入。要声明一个要延迟载入一个对象集合很简单,只需要在映射文件中相应元素添加一个特性:lazy=”true” 。 举例来说, 要使我们Deparment延迟载入集合,原来的映射文件是这样的:
<set name="Classes" cascade="all">
<key column="deptid"/>
<one-to-many class="nhRegistration.UniversityClass,
nhRegistration"/>
</set>
现在只需做如下修改就可以了:
<set name="Classes" cascade="all" lazy="true">
<key column="deptid"/>
<one-to-many class="nhRegistration.UniversityClass,
nhRegistration"/>
</set>
延迟载入的意思就是直到需要载入集合中的元素才将它们载入。这样你延迟了加载了时间,同时如果当你的执行代码从来不使用它们的时候这些集合对象根本就不会被载入。比如执行的代码可能需要访问一个具有一个或多个Processor的Department,而对于那些不符合条件的Department,就可能从不需要载入它的UniversityClass集合, 这就节省了许多时间( 无论是客户端应用还是数据库)。
但是这有一个缺点就是延迟载入需要最初装载父对象的Session来载入。要是你的代码象我的大学注册应用一样,即将所有的数据管理都放进了一个单一的数据管理类,这样你才能在在这人数据管理类中看到延迟载入好处。让我们再看一下RegMgr.getDepartments():
public IList getDepartments()
{
IList depts = null;
try
{
ISession session = factory.OpenSession();
ITransaction tx = session.BeginTransaction();
depts = session.CreateCriteria(typeof(Department)).List();
session.Close();
}
catch (Exception ex)
{
// handle exceptions
}
return depts;
}
对有力的技术缺点是,被延期的负荷需要最初的会议装载父母物体用来装载懒惰的收集。 如果你的密码跟随我们已经为大学登记申请用的式样,即分开所有的数据管理进一个单一数据经理班级,然后你才能在那一个数据经理班级中见到懒惰载入的利益。再一次看 RegMgr.getDepartments():
这里我们载入所有部门后产刻关闭了Session。当类的相关集合标记为延迟载入时,如果你尝试在业务代码中访问相关集合,这里你会获得一个“LazyInitializationException”的异常:这就意味着你既不能访问集合中的某个成员也不能获取集合中相关的合计数据如成员总数,因为它根本不存在。并且因为Session已经关闭,它将再也不会将工作,除非你将这个集合对象的父对象关联到另外一个Session。
所以,你可以选择将任何需要有益于延迟载入业务逻辑代码移入到数据管理类中,或者选择直到从这个方法返回之前都不关闭Session。我建议,除非你对这个实现费了很长时间或者有足够多的异常处理代码,否则你不要选择后者。
第四部分:拦截机制与持久对象生存周期(Interceptors and Persistent Lifecycle)
持久化对象在其生存周期中有四个主要时刻:即被装载,更新,保存和删除四个时刻( 记住, 保存是指一个新对象第一次被持久化,而更新发生在提交改时的对象时。), 更新发生当你委托对一个现有的物体变化)。 在我们已有代码中, 你能在这些时候做些事情的唯一方法是在数据管理类中相应方法中来加点你需要的代码(如getDepartment() 或 saveStudent())。但问题是,它们仅仅在数据管理类中,而如果它们是持久化对象总是需要的一部分, 这样做你就可能就将持久化对象与你的数据管理类大过紧密地联系在一起了。
如果持久化对象可以自己处理这些事刻这样会清晰。NHibernate 提供一个接口使Nhibernate在相应生存周期的时刻通知类的实例。这个接口是 ILifecycle,它是这样被定义的:
public NHibernate.LifecycleVeto OnUpdate(ISession s){}
public void OnLoad(ISession s, object id){}
public NHibernate.LifecycleVeto OnSave(ISession s){}
public NHibernate.LifecycleVeto OnDelete(ISession s){}
四个方法中有三个都会返返回一个LifecycleVeto,来决定对象下一步的生命周期事件处理。这样你就可以在保存对象之前,检查对象状态来决定更新或删作它,并且如果这些状态不符要求的条件,也可以取消对象的保存。如果取消了也就意味对象的其它操作也会失败,也就是对象不会被保存、更新或删除。
四个方法中的三个给你的物体一个机会藉由归还 LifecycleVeto 更进一步停止给定的生命周期事件的处理。使用这一个机制,你能在~之前解救质问物体的内在状态,更新或划除它,而且如果那州不符合你的申请一些标准,你能否决事件的完成。 归还否决权导致事件的其它部分默默地失败, 这意谓,物体没有在解救, 更新或划除,但是没有那一种事实的通知是申请的可得其他地方。 相反将在这些引用它们的方法中产生一个异常。
这三个方法最初是为了提供程序员自己来处理相互依赖对象之间相关系来替代Nhibernate处理的一种可选方式。 (你能更大范围对层级关联对象进行控制)但是你不应该使用Onload方法去做它,虽然你能够象NHibernate缺省行为一样。
拦截是类实现了IInterceptor 接口。这些类实现了相同类型的生存周期方法 (其它一些生存周期方法) ,但是这些在类中被定义的方法会在Session中任一一个对象的生存周期事件发生时被调用。并且在这个拦截接口中你也不能否决事件继续运行,但是你能修改对象并返回一个表明是否你做过修改的布尔值。加上在Iinterceptor中发生的事件是很多的:
public int[] FindDirty(object entity, object id,
object[] currentState, object[] previousState, string[] propertyNames, NHibernate.Type.IType[] types){}
public object Instantiate(Type type, object id){}
public bool OnFlushDirty(object entity, object id, object[]
currentState, object[] previousState, string[] propertyNames,
NHibernate.Type.IType[] types){}
public object IsUnsaved(object entity){}
public bool OnLoad(object entity, object id, object[] state, string[]
propertyNames, NHibernate.Type.IType[] types){}
public bool OnSave(object entity, object id, object[] state, string[]
propertyNames, NHibernate.Type.IType[] types){}
public void OnDelete(object entity, object id, object[] state, string[]
propertyNames, NHibernate.Type.IType[] types){}
public void PreFlush(System.Collections.ICollection entities){}
public void PostFlush(System.Collections.ICollection entities){}
在“University Registration”这个示例中,我想要实现一个一致的统一的日志机制。为了做那,我创建了一个LoggingInterceptor 的类, 它将在任何对象载入与持久化时写一个消息至日志(其他的没用方法则不记录)。
public bool OnLoad(object entity, object id, object[] state, string[]
propertyNames, NHibernate.Type.IType[] types)
{
string msg = string.Format("Instance of {0}, ID: {1} loaded.",
entity.GetType().Name, id);
log(msg);
return false;
}
public bool OnSave(object entity, object id, object[] state, string[]
propertyNames, NHibernate.Type.IType[] types)
{
string msg = string.Format("Instance of {0}, ID: {1} saved.",
entity.GetType().Name, id);
log(msg);
return false;
}
private void log(string msg)
{
// app-specific logging behavior
}
最后就要要在创建Session的时候载入一个这个Interceptor的实例以便能拦截所有对对象的相关操作。
ISession session = factory.OpenSession(new LoggingInterceptor());
这样所有的被这个Session载入的domain object的载入与保存操作都会被拦截记录下来日志。
结语:
正如你所见到的,Nhibernate有比一些映射文件和一个叫Session.Find()的更多东西,它有非常大的可扩展性。 对于小而简单的应用来说,你能使用Nhibernate来处理复杂和很多的数据访问代码,不必更多考虑我在这里谈到的各种特征。如果应用需求越来越大,越来越复杂,这里谈到将很会有用,因为你知道Nhibernate 几乎可以定制任何你想怎样进行对象持久化的特定方式。我们已经差不多对它有了一个大致的了解。
作者简介:
Justin Gehtland is a founding member of Relevance, LLC, a consultant group dedicated to elevating the practice of software development. He is the co-author of Windows Forms Programming in Visual Basic .NET (Addison Wesley, 2003) and Effective Visual Basic (Addison Wesley, 2001). Justin is an industry speaker, and instructor with DevelopMentor in the .NET curriculum.