考察下面的類結構定義html
public class Category { string _id; Category _parent; IList<Category> _children = new List<Category>(); public virtual string Id { get { return _id; } } public virtual Category Parent { get { return _parent; } } public virtual IList<Category> Children { get { return _children; } } public virtual string Title { get; set; } public virtual string ImageUrl { get; set; } public virtual int DisplayOrder { get; set; } }
其Nhibernate映射文件的內容爲:數據庫
當程序中要求「篩選出全部Category,再依次遍歷其下的Children中的子對象」時,一般,咱們會寫出以下符合要求的代碼:app
從輸出的SQL能夠看出,上面的代碼隱藏着嚴重的性能問題。假設第一級查詢返回20個Category對象保存到list列表中,而每一個Category中又包含10個子對象,那麼兩個foreach循環執行下來,共須要向數據庫發送20*10=200條Select查詢語句。對於list列表中的每一個Category來講,從數據庫中取出其自己須要執行一條Select語句(即第一級查詢),查詢其下的子元素須要執行10條Select語句,也就是說從取出Category到遍歷完其全部子對象,須要執行N+1條Select語句,N是子對象的個數,這就是所謂的「N+1」問題,它最大的弊端顯而易見,在於向數據庫發送了過多的查詢語句,形成沒必要要的開銷,而一般狀況下這是能夠優化的。性能
因爲「N+1」問題是發送了過多的Select語句,首先就會想到,能不能把這些語句合併在一次數據庫查詢中,爲了解決這個問題,Nhibernate在集合映射中,提供了「批量加載」策略,即:batch-size,經改造後的bag映射以下:fetch
batch-size代表Category.Children列表裝載時,每次讀取20個子對象,而不是一個一個加載。所以,N+1就演變成N/20+1。對於上文的兩個foreach過程,輸出的SQL語句相似於:優化
對比未採用「批量加載」策略的SQL輸出,顯然新的解決方案可以極大的減小向數據庫發送查詢語句。
若是須要將多有對象加載過程都設置爲批量,能夠在Nhibernate配置文件中添加default_batch_fetch_size屬性,而不須要修改每一個類的映射文件。
解決「N+1」問題的另外一種方法是使用預加載(Eager Fetching),一樣,Nhibernate在集合映射中也提供了對它的支持,即:outer-join或fetch。改造後的bag映射配置以下:ui
outer-join=「true」等效於fetch="join",而fetch還有「select」和「subselect」兩個選項(默認爲「select」選項),他們指的都是用何種SQL語句加載子集合。當outer-join=「true」或fetch="join"時,輸出的SQL語句相似於:spa
預加載在第一級查詢,就經過Join一次性的取出對象自己及其子對象,比使用批量加載生成的語句還要少,首一次加載效率高。hibernate
雖然,在映射文件中啓用預加載設置,十分簡單,可是考慮到其餘方式(如:Get或Load)獲取對象時也會自動裝載子對象,形成沒必要要的性能損失,另外,在映射文件中設置預加載,其「做用域」有隻適用於:經過Get或Load獲取對象、延遲加載(隱式)關聯對象、Criteria查詢和帶有left join fetch的HQL語句,所以一般要求避免將啓用預加載的配置寫在映射文件裏(Nhibernate也不推薦寫在映射文件中),而是將其寫在須要用到預加載的代碼中,其餘的地方則保持原有邏輯,這樣纔不會產生不良影響,預加載在代碼裏的寫法有三種:代理
或者在3.0裏面使用的
再或者HQL中使用left join fetch
這裏有一個奇特的狀況,FetchMode枚舉包含Eager和Join兩個選項,但實際使用中的效果是同樣的,都是輸出Join語句,沒有任何區別,Nhibernate如此設置,我猜測可能的緣由是開始時只有Join一個選項,然後以爲不夠貼切,遂增長一個Eager,但考慮到老版本兼容性,沒有刪除Join,因此就成了如今這個樣子。下面的代碼說明了在IQueryOver中如何使用預加載
到此爲止,一切都顯得很完美,不過,還沒完,預加載因爲其生成的SQL語句包括了Join或子查詢語句,所以它沒法保證獲取到集合中元素的惟一性,例如:A包含兩個子元素B和C,那麼經過預加載後,第一級查詢取出的列表中會包括兩個A對象,而不是一般咱們想象的一個。因此,啓用預加載後獲取到的列表,須要手動的解決惟一性的問題,最簡單的就是把列表裝入ISet裏「過濾」一次。
上面,咱們只假設了Category包含子對象只有一層嵌套的狀況,然而,若是子對象還有子對象,無限層嵌套時,批量加載和預加載會出現什麼狀況呢,首先,只採用批量加載的狀況下,除第一層外,如下每層嵌套都會採用批量加載的方式,可見第一層加載的效率相對較低,其次,只採用預加載的狀況下,第一次使用Join加載,獲取到第一層和第二層對象,而第二層往下,每層對象的加載過程又還原到簡單的Select上,與本文開頭所講的情形是一摸同樣的,所以,多層次加載效率較低。那麼把它們結合起來,既在映射文件中設置batch-size,又在代碼中開啓FetchMode.Eager,會不會綜合兩種的優點克服不足呢?通過實踐,答案是確定的。同時使用批量加載和預加載的狀況下,首次查詢時,SQL中出現了Join語句,即預加載起做用,獲取到第一層和第二層對象,然後每層的查詢,SQL中出現了In語句,也就是批量加載又發揮了做用,我把這種綜合運用兩種加載方式,結合了各自優勢的新方式稱爲「混合加載」,這是在Nhibernate官方文檔裏沒有的。
以上咱們談到的內容,統稱爲抓取策略(Fetching Strategy)。Nhibernate中,定義了一下幾種抓取策略:
另外,Nhibernate抓取策略會區分下列各類狀況:
默認狀況下,NHibernate對集合使用延遲select抓取,這對大多數的應用而言,都是有效的,若是須要優化這種默認策略,就須要選擇適當的抓取策略,本文第二章列出的具體的可用解決方案。
上文講述了Nhibernate的抓取策略和具體解決方案,歸結起來,在運用抓取策略提升性能時,總的原則就是:儘可能在首次查詢或每次查詢時多加載關聯的集合對象,在合適的地方使用抓取策略,既提升性能,又要影響其餘應用場景爲好。
謝謝觀賞!
1.《NHibernate Reference Documentation 3.0》:http://nhforge.org/doc/nh/en/index.html
2.Pierre Henri Kuaté, Tobin Harris, Christian Bauer, and Gavin King.Nhibernate in Action February, 2009