使用NHibernate也有近三年了,从最初的2.1一直到现在的3.3.在使用过程中犯了很多错误,走了很多弯路.最近两天又研究了一下使用细节,觉得有必要将对NH的一些认知与研究成果记录下来,作为这一段时间内的学习总结.
1.认识NH
NH并不是数据访问层的灵丹妙药,其只有在以代码为中心,使用真正的面向对象/面向领域开发时才能发挥最大威力.它能让对象以最方便,最智能的方式持久化.它不是原生Ado.Net的替代者,更不是数据库相关技术的替代者.它严格遵守80/20原则,解决程序中80%的对象存储问题.在以数据为中心的场景,如查询,统计,报表等,还是使用原生Ado.Net为易.
NH主要分为配置,映射,查询三大块.配置解决了数据库连接问题.映射解决了对象与表的关联问题.查询解决了数据获取问题.
会使用NH的API离真正掌握NH还有很大距离.NH在使用中有很多陷阱,大都出现在查询中,会对性能有严重影响.比如著名的N + 1问题,笛卡尔积问题,一次请求一次连接问题等.这要求使用者不仅会使用API,还需要了解数据库相关知识,更需要了解代码背后执行的若干原理等.其它的一些陷阱则散布在配置,映射,缓存,对象状态中等.这些给于我们的启示是:初学者不要用,熟悉者谨慎用.要么不用,要么用好!
2.相关资料
这方面的资料首推园子里李永京大哥的两个NH系列文章:
前者的版本号是2.1,后者是3.0.认真看完并动手试验过后,就基本入门了.
另外也有一些园友写了一些研究心得或某方面的专题介绍,个人觉得这些都是不可多得的好文章.
3.学习英语,努力学习英语
不是我崇洋媚外,最新的技术资料与提问解答真心只有E文的.比如全世界最大的程序员社区StackOverflow.CSDN与它比起来真心爆弱了.我的非常多的关键的疑问都是在上面获得解答的.还有各个技术框架的官网与论坛.什么Extjs, Asp.Net MVC等等.真的,如果读的懂E文,95%的事真心就不难了.
4.下载并在需要时查看源码
当你对于一个错误百思不得其解时,查看源码就是最好的选择.不要怕难,不要怕烦.看上去最笨的方法往往是最近的捷径.
5.关于Xml配置文件与配置硬编码
这两个方面在配置,映射,查询中均有体现.在早期的版本中,一般都是使用Xml配置文件方式来使用NH,如数据库配置的Hibernate.cfg.xml,对象映射的Entity.hbm.xml等,而查询则使用类似于Sql的Hql.在最初的设想中,通过xml配置后的程序,在面临环境变更时可以做到0编译.如换库,调试/日志开关,各类选项开关等.但随着认识的深入,就发现其实配置太多反而适得其反:配置是不可编译和调试的;这就意味着如果报了错,只能人工一行一行的核对;配置是缺少智能提示的,这对使用者造成了使用困难;配置缺少合适的编辑工具与构建模型,一旦超过了一定的数据,就会让使用者迷失在字符串的海洋中.一句话:Xml配置并不是万能的,要学会适用而不是滥用.所以到3.0之后,乘着.Net Lambda表达式的东风,NH就推出了多种硬编码配置:如数据库配置就多了流配置与Lambda表达式配置,而映射则多了ConfORM,ByCode等,而查询更是推出了IQueryOver接口.将大部分的很少变化的设定直接写到程序中,如数据库类型,对象映射等,而将少量开关写入Xml配置文件中,如日志/调试开关等.
本人目前使用的方式就是Lambda表达式配置 + ByCode映射配置 + IQueryOver查询接口 + Web.config配置.感觉不错~~~
6.这些年我越过的那些陷阱
a.延迟加载,抓取策略,N+1
在上面NHibernate实践总结(二)这篇文章里就有一些研究,再加上其它园友的教训与实际体验,总结就是一句话:
大部份情况下不要在配置文件里对它进行设置,而应该在程序中一次性的显式的获取你所需要的数据
默认情况下NH的Lazy=True,Fetch=Select.它的出发点很好,加载尽可能少的数据以提高性能.但这其实有非常大的局限性.对象之间是互相关联,在实际使用中很少单独使用某一对象,而是多对象一起显示,修改,删除.比如最常见的显示正在购物的客户及其手头的订单,客户可能有多个,每个客户订单可能有多个.如果使用默认的加载方式,加载完客户集合后,会循环为每个客户单独加载自己订单,这就是N+1问题产生的根源.当然,你可以在配置文件中设定加载客户的同时一并加载各自的订单.但问题在于对于其它只需客户不需订单的使用场影,同时被加载的订单是多余的.所以,我得出了上面这句结论.虽然在程序中增加了若干行代码,但这是使用可以接受的代价,获取了最大的灵活性与最好的性能.
b.查询笛卡尔积,奇怪的重复数据
比如对象A,同时与对象B,对象C关联.界面要同时显示A,B,C的数据.假设A的一条,B有2条,C有3条.按照我上面的说法,显然是加载A的同时加载B与C.下面是使用IQueryOver的语法
session.QueryOver<A>()
.Fetch(B).Eager
.Fetch(C).Eager
.SingleOrDefault()
很好,看上去语法没有错.即使是使用原生的Sql也会三表直接关联查询.但是这却不是性能最好的查询.因为在返回的记录集中,它会查出6条记录.其中A对象部分完全重复,B对象部分重复三次,C部分重复两次,但单看每一条记录,却又不是完全重复的.Ok,这就是传说中的笛卡尔积结果!其实还有更悲剧的结果.如果在NH映射中为B与C使用的是Bag,那么你就会发现在查出的结果中A对象有6个B对象与6个C对象!其中B重复三次C重复两次,与查询结果完全一致!什么,它不会自动去重吗?
这个问题,我之前在看文档看例子没有任何在意,只有真真实实遇到了才有恍然大悟的感觉.NH给你提供了四种映射集合类型不是白给的,每一种都有它的适用范围.对于后面一个问题,有两种解决方法,要么在映射中使用Set,要么在程序多加一句:
session.QueryOver<A>()
.Fetch(B).Eager
.Fetch(C).Eager
.TransformUsing(Transformers.DistinctRootEntity)
.SingleOrDefault()
Eagerly fetch multiple collection: differences between QueryOver and Query
对于前一个问题,只有改进查询方式,如下:
var aFuture = session.QueryOver<A>() .Fetch(B).Eager
.TransformUsing(Transformers.DistinctRootEntity) .FutureValue(); session.QueryOver<A>() .Fetch(C).Eager .TransformUsing(Transformers.DistinctRootEntity) .Future(); var result = aFuture.Value;
NHibernate - Querying relationships at depth!
Eagerly fetch multiple collection properties (using QueryOver/Linq)?
Eagerly fetch multiple collection: differences between QueryOver and Query
fetching multiple nested associations eagerly using nhibernate (and queryover)
NHibernate lazy loading nested collections with futures to avoid N+1 problem
NHibernate Pitfalls: Eager Loading Multiple Collections
c.为什么NH自动生成的查询使用的都是Left Out Join
说实话我一开始没有注意这个问题,后来在碰到其它问题,想将这个连接换成Inner时才发现这个情况.我自己想了半天不明所以,在网上查了半天才恍然大悟:
为了保证所有满足条件的根对象被查出来.
比如A对象,关联了B对象.有些A对象有多个B对象,有些则一个没有.当你联合查询所有A,B对象时你期望的结果是所有的A都能被查出,关联了B对象则B对象有值,反之为空.如果使用Inner关联,则只会查出所有关联了B对象的A对象.
默认情况下这个Left Out Join连接是不可更改的.所以你如果真的想更换连接,则需要在程序设置.
还有,只有使用了Left Out Join,Fetch设置为Join模式才会生效.而使用其它连接方式,强制使用Lazy=True,Fecth=Select,而不管你实际使用的是什么.这些都会导致N + 1问题.
Inner or Right Outer Join in Nhibernate and Fluent Nhibernate on Many to Many collection
d.多对多中奇怪的空记录
这个问题只有使用Xml配置方式才会出现.因为默认提供的硬编码根本就不给你这个选项.当然,HN都是开源的,你自己是可以改滴!
在多对多配置中,有一个where选项,如下:
<many-to-many where="" class="" column=""></many-to-many>
它想表达的意思是:你可以为另一个多的一方加上Sql条件.如,我们在界面上放置的删除按钮,通常都是逻辑删除,即在对象中加入一个IsDeleted字段,删除这个对象,就是将IsDeleted字段改为True.那么我在配置NH时,会在这个where中加入"IsDeleted = 0"来过滤这些已被删除的记录.假设A对象多对多关联了三个B对象,其中一个B对象的IsDeleted字段为True.在实际查询中,会很诡异的查出三条记录,但只有前两条有数据,第三条为空,而其所对应的ISet集合,居然也有三个元素,但前两个元素有值,第三个为null.
我研究了其生成的查询语句,它将这个Where条件放在了连接条件中,而不是最终的Where子句中.这完全不符合我的本意啊!我想这也是为什么在新的ByCode配置中将其删除的原因.
我现在的做法是,不在映射里配置,而是在程序中手工加上过滤条件.
e.Many方法的Insert,Update与One方的Inverse
这个问题也困扰了我很长时间,园子里有一篇文章写的很好,而我也就直接说结论了.
在Many方法设置Insert=False, Update=False,NotFound=Ignore,在One方设置Inverse=True
f.保存的各个方法的含义
这个问题也困扰了我很长时间,当然,园子里仍然有一篇文章写的很好,而我也再次直接说结论了
使用NH,大部分情况下严格遵守NH使用三步曲:加载,更新,保存.在大部分情况下,只需要使用Save方法.
NHibernate的各种保存方式的区别 (save,persist,update,saveOrUpdte,merge,flush,lock)
g.关联表使用独立主键
这个也让我烦恼了很长时间,当然,这是我自己的问题,看文档不仔细.使用IdBag就可以解决这个问题!
Nhibernate 3.0 cookbook学习笔记 集合
h.一次请求一次连接
我翻译的太土了,其E文名叫One Session Per Request.我发现还有很多人都在自己实现这个功能,我也曾经试图造过重复的轮子,但实际上NH早就自带相关特性了.具体请参看下面这篇文章:
NHibernate Session Management in ASP.NET MVC
《NHibernate One Session Per Request 简单实现》勘误
i.可重写的日志
从NH3开始,移除了对Log4Net的依赖,可以使用任意日志组件了.不过需要注意的是,NH中有很多个日志记录器,只有名为NHibernate.SQL的日志记录器才记录所有生成的Sql语句.且这个名字是不可改变的!切记!下面就是几篇参考的文章.
Using NLog via Common.Logging with NHibernate
How do NHibernate Profiler support NHibernate 3 logging
Capture NHibernate generated SQL Query realtime at runtime
Simple logger for NHibernate 3
j.扩展自己的ByCode
这个问题是我在多对多映射中ByCode无法配置Where节所遇到的,本想通过继承而不是修改源代码来完成,但没有成功.参看下面两篇如何修改源代码的文章吧.
How to implement .ChildWhere() mapping with many-to-many relation in NH 3.2
Where() clause with many-to-many relation is missing (solution in description)
但如上面所说的,这个Where节自身有缺陷,而我最终也放弃了扩展.
匆忙间已挖不出更多的坑,也想不到更多的经验,只好搁笔于此.如果以后再想到相关内容,再自行补上.虽然用好NH不易,但这并不妨碍其成为.Net下最优秀的ORM框架.什么iBatis啊,EF啊,都是浮云.
向为全世界做出卓越贡献的Hibernate框架与NHibernate框架的开发者们献上我最崇高的敬意!你们辛苦啦!