NHibernate的認知,總結與陷阱
使用NHibernate也有近三年了,從最初的2.1一直到現(xiàn)在的3.3.在使用過程中犯了很多錯誤,走了很多彎路.最近兩天又研究了一下使用細節(jié),覺得有必要將對NH的一些認知與研究成果記錄下來,作為這一段時間內的學習總結.
1.認識NH
NH并不是數據訪問層的靈丹妙藥,其只有在以代碼為中心,使用真正的面向對象/面向領域開發(fā)時才能發(fā)揮最大威力.它能讓對象以最方便,最智能的方式持久化.它不是原生Ado.Net的替代者,更不是數據庫相關技術的替代者.它嚴格遵守80/20原則,解決程序中80%的對象存儲問題.在以數據為中心的場景,如查詢,統(tǒng)計,報表等,還是使用原生Ado.Net為易.
NH主要分為配置,映射,查詢三大塊.配置解決了數據庫連接問題.映射解決了對象與表的關聯(lián)問題.查詢解決了數據獲取問題.
會使用NH的API離真正掌握NH還有很大距離.NH在使用中有很多陷阱,大都出現(xiàn)在查詢中,會對性能有嚴重影響.比如著名的N + 1問題,笛卡爾積問題,一次請求一次連接問題等.這要求使用者不僅會使用API,還需要了解數據庫相關知識,更需要了解代碼背后執(zhí)行的若干原理等.其它的一些陷阱則散布在配置,映射,緩存,對象狀態(tài)中等.這些給于我們的啟示是:初學者不要用,熟悉者謹慎用.要么不用,要么用好!
2.相關資料
這方面的資料首推園子里李永京大哥的兩個NH系列文章:
前者的版本號是2.1,后者是3.0.認真看完并動手試驗過后,就基本入門了.
另外也有一些園友寫了一些研究心得或某方面的專題介紹,個人覺得這些都是不可多得的好文章.
3.學習英語,努力學習英語
不是我崇洋媚外,最新的技術資料與提問解答真心只有E文的.比如全世界最大的程序員社區(qū)StackOverflow.CSDN與它比起來真心爆弱了.我的非常多的關鍵的疑問都是在上面獲得解答的.還有各個技術框架的官網與論壇.什么Extjs, Asp.Net MVC等等.真的,如果讀的懂E文,95%的事真心就不難了.
4.下載并在需要時查看源碼
當你對于一個錯誤百思不得其解時,查看源碼就是最好的選擇.不要怕難,不要怕煩.看上去最笨的方法往往是最近的捷徑.
5.關于Xml配置文件與配置硬編碼
這兩個方面在配置,映射,查詢中均有體現(xiàn).在早期的版本中,一般都是使用Xml配置文件方式來使用NH,如數據庫配置的Hibernate.cfg.xml,對象映射的Entity.hbm.xml等,而查詢則使用類似于Sql的Hql.在最初的設想中,通過xml配置后的程序,在面臨環(huán)境變更時可以做到0編譯.如換庫,調試/日志開關,各類選項開關等.但隨著認識的深入,就發(fā)現(xiàn)其實配置太多反而適得其反:配置是不可編譯和調試的;這就意味著如果報了錯,只能人工一行一行的核對;配置是缺少智能提示的,這對使用者造成了使用困難;配置缺少合適的編輯工具與構建模型,一旦超過了一定的數據,就會讓使用者迷失在字符串的海洋中.一句話: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.它的出發(fā)點很好,加載盡可能少的數據以提高性能.但這其實有非常大的局限性.對象之間是互相關聯(lián),在實際使用中很少單獨使用某一對象,而是多對象一起顯示,修改,刪除.比如最常見的顯示正在購物的客戶及其手頭的訂單,客戶可能有多個,每個客戶訂單可能有多個.如果使用默認的加載方式,加載完客戶集合后,會循環(huán)為每個客戶單獨加載自己訂單,這就是N+1問題產生的根源.當然,你可以在配置文件中設定加載客戶的同時一并加載各自的訂單.但問題在于對于其它只需客戶不需訂單的使用場影,同時被加載的訂單是多余的.所以,我得出了上面這句結論.雖然在程序中增加了若干行代碼,但這是使用可以接受的代價,獲取了最大的靈活性與最好的性能.
b.查詢笛卡爾積,奇怪的重復數據
比如對象A,同時與對象B,對象C關聯(lián).界面要同時顯示A,B,C的數據.假設A的一條,B有2條,C有3條.按照我上面的說法,顯然是加載A的同時加載B與C.下面是使用IQueryOver的語法
session.QueryOver<A>()
.Fetch(B).Eager
.Fetch(C).Eager
.SingleOrDefault()
很好,看上去語法沒有錯.即使是使用原生的Sql也會三表直接關聯(lián)查詢.但是這卻不是性能最好的查詢.因為在返回的記錄集中,它會查出6條記錄.其中A對象部分完全重復,B對象部分重復三次,C部分重復兩次,但單看每一條記錄,卻又不是完全重復的.Ok,這就是傳說中的笛卡爾積結果!其實還有更悲劇的結果.如果在NH映射中為B與C使用的是Bag,那么你就會發(fā)現(xiàn)在查出的結果中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時才發(fā)現(xiàn)這個情況.我自己想了半天不明所以,在網上查了半天才恍然大悟:
為了保證所有滿足條件的根對象被查出來.
比如A對象,關聯(lián)了B對象.有些A對象有多個B對象,有些則一個沒有.當你聯(lián)合查詢所有A,B對象時你期望的結果是所有的A都能被查出,關聯(lián)了B對象則B對象有值,反之為空.如果使用Inner關聯(lián),則只會查出所有關聯(lián)了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配置方式才會出現(xiàn).因為默認提供的硬編碼根本就不給你這個選項.當然,HN都是開源的,你自己是可以改滴!
在多對多配置中,有一個where選項,如下:
<many-to-many where="" class="" column=""></many-to-many>
它想表達的意思是:你可以為另一個多的一方加上Sql條件.如,我們在界面上放置的刪除按鈕,通常都是邏輯刪除,即在對象中加入一個IsDeleted字段,刪除這個對象,就是將IsDeleted字段改為True.那么我在配置NH時,會在這個where中加入"IsDeleted = 0"來過濾這些已被刪除的記錄.假設A對象多對多關聯(lián)了三個B對象,其中一個B對象的IsDeleted字段為True.在實際查詢中,會很詭異的查出三條記錄,但只有前兩條有數據,第三條為空,而其所對應的ISet集合,居然也有三個元素,但前兩個元素有值,第三個為null.
我研究了其生成的查詢語句,它將這個Where條件放在了連接條件中,而不是最終的Where子句中.這完全不符合我的本意啊!我想這也是為什么在新的ByCode配置中將其刪除的原因.
我現(xiàn)在的做法是,不在映射里配置,而是在程序中手工加上過濾條件.
e.Many方法的Insert,Update與One方的Inverse
這個問題也困擾了我很長時間,園子里有一篇文章寫的很好,而我也就直接說結論了.
在Many方法設置Insert=False, Update=False,NotFound=Ignore,在One方設置Inverse=True
f.保存的各個方法的含義
這個問題也困擾了我很長時間,當然,園子里仍然有一篇文章寫的很好,而我也再次直接說結論了
使用NH,大部分情況下嚴格遵守NH使用三步曲:加載,更新,保存.在大部分情況下,只需要使用Save方法.
NHibernate的各種保存方式的區(qū)別 (save,persist,update,saveOrUpdte,merge,flush,lock)
g.關聯(lián)表使用獨立主鍵
這個也讓我煩惱了很長時間,當然,這是我自己的問題,看文檔不仔細.使用IdBag就可以解決這個問題!
Nhibernate 3.0 cookbook學習筆記 集合
h.一次請求一次連接
我翻譯的太土了,其E文名叫One Session Per Request.我發(fā)現(xiàn)還有很多人都在自己實現(xiàn)這個功能,我也曾經試圖造過重復的輪子,但實際上NH早就自帶相關特性了.具體請參看下面這篇文章:
NHibernate Session Management in ASP.NET MVC
《NHibernate One Session Per Request 簡單實現(xiàn)》勘誤
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節(jié)所遇到的,本想通過繼承而不是修改源代碼來完成,但沒有成功.參看下面兩篇如何修改源代碼的文章吧.
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節(jié)自身有缺陷,而我最終也放棄了擴展.
匆忙間已挖不出更多的坑,也想不到更多的經驗,只好擱筆于此.如果以后再想到相關內容,再自行補上.雖然用好NH不易,但這并不妨礙其成為.Net下最優(yōu)秀的ORM框架.什么iBatis啊,EF啊,都是浮云.
向為全世界做出卓越貢獻的Hibernate框架與NHibernate框架的開發(fā)者們獻上我最崇高的敬意!你們辛苦啦!

浙公網安備 33010602011771號