前面的兩篇反應很差:沒評論沒贊。很傷心啊,為什麼呢?搞得我好長一段時間都沒更新了——呵呵,好吧,我承認,這隻是我的借口。不過,還是希望大家多給回報。沒有回報,我就隻能猜了:前面兩篇是不是寫得太“粗”了一點?是以這一篇我們盡量詳細點吧。
這是一個使用NHibernate建構Web項目慣用的模式,相關的文章其實很多。我盡量用我的語言(意思是大白話,但可能不精确)來做一個簡單的解釋。
首先,你得明白什麼是session。這不是ASP.NET裡面的那個session,初學者在這一點上容易犯暈。這是NHibernate的概念。
如果你對它特别感興趣的話,你可以首先搜尋“Unit Of Work”關鍵字,了解這個模式;然後逐漸明白:session其實是NHibernate對Unit Of Work的實作。
如果你隻想了解一個大概,那麼你可以把它想象成一個臨時的“容器”,裝載着從資料庫取出來的entity,并一直記錄其變化。
如果你還是覺得暈乎,就先把它當成一個打開的、活動的資料庫連接配接吧。
我們都知道資料庫連接配接的開銷是很大的,為此.NET還特别引入了“連接配接池”的概念。是以,如果能有效的降低資料庫的連接配接數量,對程式的性能将有一個巨大的提升作用。經過觀察和思考,大家(我不知道究竟是誰最先提出這個概念的)覺得,一個HTTP request配置設定一個資料庫連接配接是一個很不錯的方案。于是Session Per Request就迅速流行起來,幾乎成為NHibernate建構Web程式的标配。
如果為了性能,破壞了代碼的可維護性,那麼我們甯願不要性能;
在能夠保證可維護性的前提下,我們當然應該努力的提高性能;
較之于在局部(非性能瓶頸處)糾結發力,不如在架構的層面上保證/促進整體性能的提高。
我說提到的“性能的問題先不管”,以及“忘記資料庫”等,是基于矯枉必須過正的出發點,希望能夠有振聾發聩的效果。但結果看來不是很好,評論裡我還是看到了“SELECT TOP 1 * FROM TABLE WHERE ID>CURRRID”之類的東西。這說明什麼?關系資料庫不但已經在你腦子裡紮根,而且已經把你腦子都塞滿了。不是說這樣不行,隻是這樣的話,實在沒辦法和你談論面向“對象”。
Session Per Request就是一個已經被廣泛采用,行之有效的,能在架構層面提升性能的一個設計。
我們僅從Session Per Request的定義,什麼Http啊,Request啊,憑直覺就能想到UI層的範疇吧?
網上的很多示例都确實是這麼寫的。在Application裡BuildSessionFactory,在HttpModule中配置:一旦HTTP request到達,就生成一個session;Http request結束,就調用session.flush()同步所有更改。
但是,我們在架構中就已經确立了這樣一個原則:UI層不涉及資料庫操作。更直覺的看,UI層的project連NHibernate.dll的引用都沒有。那怎麼辦呢?
現在想來很好笑,當年我可是費了不少的腦細胞:其實隻需要在Service層封裝相關的操作,然後在UI層調用Service層即可。
那些把我繞暈了的不靠譜的想法大家可以不用去理會了。如果确實有興趣,可以思考一下:NHibernate中session是有上下文環境(context)的,我們這裡當然應該設定成web,但Service最後會被編譯成一個dll,這個dll裡能取到HttpContext麼?
但在Service裡怎麼封裝,也是一件值得斟酌的事。
我最後采用的方案是引入BaseService:
首先,在BaseService中設定一個靜态的sessionFactory;而且,在BaseService的靜态構造函數中給sessionFactory指派(Build SessionFactory)。這樣,就可以保證SessionFactory隻生成一次,因為生成SessionFactory是一個開銷很大的過程。

引入sessionFactory
其次,在BaseService中暴露一個靜态的EndSession()方法,在Request結束時将資料的變化同步到持久層(資料庫)。是以當UI層調用時,不需要執行個體化一個BaseService,隻需要BaseService直接調用即可:

EndSession
然後,我們回頭看看前面的說法:“一旦HTTP request到達,就生成一個session;”,是以理論上需要一個InitSession()的方法,生成/提供一個session。但我突然有了點小聰明:有些頁面可能是不需要資料庫操作的,比如幫助、表單呈現,或者其他我們暫時想不到的頁面。那我們無論如何總是生成一個session,是不是浪費了點?
越想越覺得是這麼一回事,是以左思右想,弄出了一個方案:按需生成session。大緻的流程是:
嘗試擷取session;
如果“目前環境”中已有一個session,就直接使用該session;
否則就生成一個session,使用該session,并将其存入目前環境中。
看來NHibernate支援這種思路,是以提供了現成的接口,可以很友善的實作上述思路:

按需擷取session
其中CurrentSessionContext就是上文所謂的“目前環境”,在我們的系統中國就是一個HttpContext;我們使用GetCurrentSession()就總是能夠保證取出的session是目前HttpContext中已有的session。所有的Service都繼承自BaseService,直接調用BaseService中的session,這樣就可以有效的保證了Session Per Request的實作。
同學們,這下知道了吧?其實我骨子裡還是一個很“摳”性能的人。但這樣做究竟值不值?我也不太确定,畢竟這樣做一定程度上增加了代碼的複雜性,而所獲得的性能提升其實有限。
如果同學們檢視源代碼,就會發現,我們的session總是啟用了事務。

總是使用事務
在我們傳統的觀念中,使用“transaction”,會增加資料庫的開銷,降低性能。但實際上并不是這樣的,至少我可以保證在NHibernate和Mysql中不是這樣的。
大緻的原因有幾點:
即使不顯式的聲明事務,資料庫也會顯式的生成一個事務;
NHibernate的二級緩存需要事務做保證
其實,既然使用了Session Per Request模式,我們即使從業務邏輯上考慮,也應該總是使用“事務”:很多時候一次表單送出要執行多個資料庫操作,一些步驟執行了一些報了異常,資料不完整咋辦?
前面已經反複說過,在Service中,沒有資料庫的Update操作。我們是通過:Load()資料 -> 改變其屬性 -> 然後在Save()到資料庫來實作的。
但同學們檢視我們的源代碼的時候會發現:“咦?怎麼沒有Session.Save()這樣一個過程?”
首先,大家應該了解NHibernat中的Update()不是我們大多數同學想象的那樣,對應着sql裡的update語句。它實際上用于多個session互動時的場景,我們目前的系統是永遠不會使用的。
然後,NHibernate也不是使用session.Save()來同步session中的資料到資料庫的。我們系統中隻是偶爾使用session.Save()來暫時的獲得entity的Id。
最後,NHibernate中實際上是使用session.Flush()來最終“同步”記憶體(session)中的資料到資料庫的。而我們代碼中使用的是session.Transaction.Commit(),這會自動的調用session.Flush()。
因為Session Per Request模式,我們在UI層中,總是會在request結束時調用EndSession(),是以在Service的代碼中,看起來就沒有了“存儲”資料的過程。
那麼,在UI層的哪裡調用EndSession()呢?(因為按需生成session,已經不需要BeginSession()了)
大緻來說,有兩種方案,一種是使用HttpModule,另一種是利用ASP.NET MVC的filter機制。
我們采用了後者,一則是這樣更簡單,另一方面是因為:當引入ChildAction之後,從邏輯上講,Session Per Action更自洽一些。比如一個Request可能包含多個Child Action,将多個Child Action放在一個session裡,可能出現難以預料的意外情況。
當然,這樣做的不利的一面就是會消耗更多的session,但好在session的開銷很小,而且我們使用的“按需生成session”可以降低一些session生成情景。
代碼非常簡單,如下:

調用EndSession()
#if PROD的使用是為了前後端分離(後文詳述):隻有當調用ProdService時才使用以上代碼,UI開發人員使用UIDevService時不需要改項操作。
同時,為了避免反複的聲明,我們提取出BaseController,由所有Controller繼承,并在BaseController上聲明SessionPerRequest即可:

SessionPerRequest聲明
由于我們在Action呈現後實作資料的同步(session.Transaction.Commit()),是以我們所有的Ajax調用,沒有使用Web API,而是繼承自ActionResult的JsonResult。否則,不會觸發OnResultExecuted事件,也無法同步資料庫。

AJAX傳回JsonResult
綜上,我們實際上是借鑒了SessionPerRequest的思路,實際上采用了按需生成Session、且一個Action使用一個session的實作。可以描述成:SessionPerActionIfRequire,呵呵。
通過SessionPerRequest,我們可以發現架構的一個重要作用:将系統中“技術複雜”的部分封裝起來,讓開發人員可以脫離複雜瑣碎的技術,而專注于具體業務的實作。事實上,采用我們的系統,即使一個不怎麼懂NHibernate的普通開發人員,經過簡單的介紹/教育訓練,也可以迅速的開始業務領域代碼的編寫工作。
+++++++++++++++++++++++++++++
應該是2016年春節前最後一篇《架構之路》的更新了,先預祝大家新春快樂,萬事如意!
另外,歡迎各種留言評論(包括拍磚)。
O(∩_∩)O~