NHibernate本身並不是一個數據庫。它是一個輕量級的對象-關系映射工具。因此,它的事務管理代理給對應的數據庫連接。如果這個連接代理了一個分布式的事務,ISession管理的操作就會自動成為整個分布式事務的一部分。NHibernate可以被當作是一個簡單的ADO.NET的配適器,再加上一些面向對象的語法。
配置,session和工廠
ISessionFactory是一個耗資源的線程安全對象,因此被所有應用程序線程共享。一個ISession是一個一次性的耗資源的線程不安全對象,對應一個單獨的業務過程,業務結束之后就被拋棄。例如,當在ASP.NET應用程序中使用NHibernate的時候,頁面可以獲得一個ISessionFactory,通過以下方式:
每次調用這個方法都會創造一個ISession,你可以Flush() 它, Commit()它的事務, Close() 它會最終拋棄它。(ISessionFactory會被存放在一個靜態的單利幫助變量中)。
我們像之前討論的那樣來使用NHibernate的ITransaction API,一個NHibernate ITransaction 的Commit()方法將會flush整個狀態,然后將對應的數據庫連接中的操作提交(在分布式事務中會有特殊處理)。
要確保你理解Flush()的含義。flushing操作將內存中的改變同步到數據庫中,但是不會反過來將數據庫的改變同步到內存里。要注意的是,對於所有的NHibernate ADO.NET 連接/事務,對這個連接的事務獨立等級會應用到NHibernate執行的所有操作中。
下面的幾個小節中,我們會討論其他通過使用版本來保證事務的原子性的方法。這些方法都是比較先進的方法,但是使用的時候需要小心。
線程和連接
你在產生NHibernate session的時候應當研究下面這些東西:
- 永遠不要為一個數據庫連接創建多余一個並發的ISession或者ITransaction
- 在為一個數據庫的一個事務創建一個Isession的時候一定要非常小心。ISession自身會最總加載到內存中的數據的變更情況,因此不同的ISession中可能會看到舊數據。
- ISession不是線程安全的。在兩個並發線程中不要同時操作同一個ISession。一個ISession通常僅僅是一個工作單元。
考慮模型一致
應用程序可能在兩個不同的工作單元中同時獲取同一個持久化狀態。然而,一個持久化類的實例永遠不能再兩個ISession實例中共享。因此,有兩個不同的標識符概念:
The application may concurrently access the same persistent state in two different units-of-work. However, an instance of a persistent class is never shared between two ISession instances. Hence there are two different notions of identity:
- 數據庫標識符
-
foo.Id.Equals( bar.Id )
- CLR標識符
foo == bar
- 對於關聯某個特定Session的對象們來說,這兩個概念是一致的。然而,應用程序可能在兩個不同的session中並發地獲取“相同的”(持久化身份)業務實體,這兩個實例實際上是不同的(CLR身份)。
- 這種方法為NHibernate和數據庫帶來並發的問題。應用程序永遠不需要同步其他的業務對象,如果它嚴格地使每個線程對應每個ISession或者對象身份(在一個ISession中,應用程序可以安全地使用==來比較對象)。
樂觀並發控制
很多業務過程需要一個用戶與數據庫的各種交互。然而,在web或者企業應用程序中,it is not acceptable for a database transaction to span a user interaction。
保證業務過程的獨立性成為了應用程序的一個責任,因此我們把這種保證業務過程獨立性的過程叫做長時間運行的應用程序事務。一個單獨的應用程序事務通常會橫跨幾個數據庫事務。然而,只有在一個數據庫事務(並且是最后一個)保存更新后的數據,而其他所有數據庫只進行讀數據操作的時候,它才是原子的。
針對這種高並發和高伸縮性系統的系統,唯一滿足的設計方法就是通過版本來實現樂觀並發控制。NHibernate提供了三個有效的方法。
自動生成版本信息的長session方式
整個應用程序事務中,只有一個ISession實例和他的持久化實例。
ISession使用帶版本的樂觀鎖來保證許多應用程序中的數據庫事務是一個單一的邏輯應用事務。ISession在等待用戶交互的時候斷開相應的ADO.NET連接。這種數據庫連接的方式最有效。應用程序不需要關心它自身的數據版本檢查或者是否重新關聯游離的實例。
foo對象仍然知道哪一個ISession加載了它。只要這個ISession保持ADO.NET的連接,我們就可以把對象的改變提交給數據庫。
; session.Flush(); transaction.Commit(); session.Disconnect();
但是,如果我們的ISession在用戶思考的時候過於龐大以至於不適合存儲在內存里,例如一個HttpSession應該盡可能地小,這種模式就會產生問題。因為ISession同時也是(強制的)一級緩存並且包含各種已經加載到內存里面的對象的時候,我們可能只能在一些非常少的request/response周期中使用這種方式。這個是一個推薦的方法,雖然ISession也會有可能產生過期的數據。
自動生成版本信息的多session方式
每一個和數據庫交互都在一個新的ISession中發生。然而,同樣的持久化實例在每一個和數據庫交互中重復使用。應用程序控制在其他ISession加載的游離實例的狀態,然后通過使用ISession.Update() 或者 ISession.SaveOrUpdate()來重新關聯它們。
session.BeginTransaction(); session.SaveOrUpdate(foo); session.Flush(); transaction.Commit(); session.Close();
如果你確定實體沒有經過任何的修改,你也可以調用Lock() 而不是 Update() 方法,然后使用LockMode.Read 方法(忽視所有的緩存執行版本檢查)。
自定義版本信息生成方式
對一些屬性和集合,你可以通過將optimistic-lock 的mapping特性設置成 false來關閉NHibernate的自增長版本信息功能。這樣NHibernate就不會為臟屬性增長版本信息了。
數據庫表空間通常不能修改。或者其他應用程序可能也連接到了相同的數據庫,然而不知道如何處理版本信息或者是時間戳。在這兩種情況下,版本不能僅僅依賴表中的特定字段。為了強制進行版本的校驗,盡管並不存在版本或者時間戳屬性的mapping,通過比較一條數據中的所有字段的狀態,在<class> mapping中將optimistic-lock設置成"all" 。需要注意的是,這個方式理論上只在NHibernate能夠分辨新和舊的狀態的時候有效,例如,如果你使用一個長時間運行的ISession並且沒有針對每一個游離對象請求對應一個session。
在一些情況下,只要修改的東西沒有重疊,並發的修改操作都是可以被允許的。如果你在<class> mapping中將optimistic-lock設置成"all,NHibernate在進行flush操作的時候就僅僅比較臟字段。
無論哪種情況,通過精細的版本/時間戳字段或者通過完整/臟字段的比較,NHibernate通過針對每一個實體使用一個UPDATE聲明(包含相應的where限定語句)來執行版本檢查和字段更新。如果你使用級聯操作的持久化方式來重新關聯相關的實體,NHibernate就可能會執行一些不必要的更新。這個通常不是一個問題,但是on update會導致數據庫執行一些操作,哪怕對於瞬時態的對象並沒有任何修改操作。你可以通過在<class> mapping中將select-before-update設置成"true"來自定義這個行為,這樣能讓NHibernate先select這個實例來查看是否真的在更新操作之前發生了修改行為。
應用程序版本檢查
每一個與數據庫的交互都在一個新的ISession中產生,這個Isession在操作這些實例之前都會重新加載所有的持久化實例。這個操作迫使應用程序來進行自身的版本檢查來保證應用程序事務的獨立(當然,NHibernate會為你更新版本信息)。這個方法是數據庫交互方式中效率最低的方法。
; session.Flush(); transaction.Commit(); session.close();
當然,如果你在一個低並發的環境下病情不需要版本檢查,你可以通過使用這個辦法來跳過版本檢查步驟。
斷開session連接
上面介紹的第一個方法是在用戶思考的時候(例如,一個servlet(java?!!)可能會在用戶的httpsession中存放一個ISession)對整個業務過程維護一個單一的ISession。為了保證性能,你需要
- 執行這個ITransaction然后
- 關閉ISession的ADO.NET連接
- 在等待用戶響應之前。ISession.Disconnect() 方法會斷開session中的ADO.NET連接然后把連接放回連接池中(除非是你提供的連接而不是連接池)。
ISession.Reconnect() 會獲得一個新的連接然后重新開啟session。重新連接之后,為了強制進行版本檢查,你不能進行更新操作,你可以在任何可能已經在其他事務中更新對象上調用ISession.Lock() 。你不需要鎖住任何你正在更新的數據。
這里是一個例子
{ tx
=
s.BeginTransaction()) fooList =
s.CreateQuery( "select foo from Eg.Foo foo where foo.Date = current date" // uses db2 date function ).List<Foo>
(); bar = new
Bar(); s.Save(bar); tx.Commit(); } catch
(Exception) { if (tx != null
) tx.Rollback(); s.Close(); throw
; } s.Disconnect();
然后
{ tx
=
s.BeginTransaction(); bar.FooTable = new
HashMap(); foreach (Foo foo in
fooList) { s.Lock(foo, LockMode.Read); //check that foo isn't stale
bar.FooTable.Put( foo.Name, foo ); } tx.Commit(); } catch
(Exception) { if (tx != null
) tx.Rollback(); throw
; } finally
{ s.Close(); }
你可以從這里看到,ITransactions 和ISessions 是many-to-one關系。一個ISession代表應用程序和數據庫的會話。ITransaction 將這個會話在數據庫級別上拆成了原子工作單元。
樂觀鎖
我們並不希望用戶花過多的時間在擔心鎖的問題上。通常指定一個ADO.NET的隔離等級然后讓數據庫去做其他的工作就行了。然而,高級用戶可能在一些時候想要獲得排他的樂觀鎖,或者在一個新的事務開始的時候重新獲得鎖。
NHibernate將會一直使用數據庫的鎖機制,而不會在內存中鎖住對象。
LockMode類定義了NHibernate可能獲得的不同所得等級。一個鎖會通過下面的這些機制獲得:
- LockMode.Write 會在NHibernate更新或者插入一個數據的時候自動獲得
- LockMode.Upgrade 可能會在用戶在支持“SELECT ... FOR UPDATE ”語法的數據庫中直接使用SELECT ... FOR UPDATE 的時候獲得。
- LockMode.UpgradeNoWait 可能會在用戶在oracle中使用SELECT ... FOR UPDATE NOWAIT
- LockMode.Read 在NHibernate在Repeatable Read or Serializable 隔離等級下讀數據的時候自動獲得。可能在用戶的一些直接請求的時候再次獲得。
- LockMode.None 表示沒有鎖。所有對象在一個ITransaction結束的時候轉換到這個鎖模式。對象通過調用Update() 或者 SaveOrUpdate() 方法來產生關聯也會以這種模式開始。
-
所謂的“用戶的直接請求”是下面幾種方式之一:
- 調用ISession.Load(),指定一個LockMode
- 調用ISession.Lock()
- 調用IQuery.SetLockMode()
如果ISession.Load()在Upgrade or UpgradeNoWait模式下調用,並且被請求的對象在session中還沒有被加載,這個對象就是使用SELECT ... FOR UPDATE語句來加載的。如果為一個已經加載的對象調用Load() 方法,並且這個對象比請求對象鎖的限定性更小,NHibernate就會為這個對象調用Lock()方法。
ISession.Lock()提供了一個版本數字來檢查特定的鎖模式是Read, Upgrade 還是 UpgradeNoWait(在Upgrade 或者 UpgradeNoWait情況下,使用SELECT ... FOR UPDATE )
如果數據庫不支持請求的鎖模式,NHibernate就會使用其他合適的模式(而不是拋出異常)。這樣保證了應用程序的輕便性。
釋放連接模式
關於1.0版本之后的NHibernate對於ADO.NET連接管理方式,在首次需要一個ISession的時候,ISession會獲得一個連接,然后保持這個連接直到session關閉。Nhibernate引入了釋放連接模式來告訴session如何管理它的ADO.NET連接。需要注意的是,下面的討論僅僅是關於通過配置好的IConnectionProvider提供的連接;用戶提供的連接不在討論范圍內。不同的模式配置是根據NHibernate.ConnectionReleaseMode這個枚舉來區分的:
-
OnClose -這種模式在前文介紹的管理方式中十分重要。NHibernate的session在它初次被創建的時候獲得一個連接來實現一些數據庫連接,並且保持這個連接直到這個session被關閉。
-
AfterTransaction - 事務完成后釋放連接。
配置參數hibernate.connection.release_mode 主要用來指定用來釋放連接的模式,可能的值有:
-
auto (the default) - 在當前的釋放模式中相當於after_transaction 。通常來說,改變這個默認行為不是一個好主意,因為改變這個值可能會造成一些bug。
-
on_close - says to use ConnectionReleaseMode.OnClose. This setting is left for backwards compatibility, but its use is highly discouraged.相當於使用ConnectionReleaseMode.OnClose。這個淚痣主要是為了向后兼容,十分不建議使用。
-
after_transaction - says to use ConnectionReleaseMode.AfterTransaction. Note that with ConnectionReleaseMode.AfterTransaction, if a session is considered to be in auto-commit mode (i.e. no transaction was started) connections will be released after every operation.相當於使用ConnectionReleaseMode.AfterTransaction。需要注意的是,在這個模式下,如果一個session被認為是在自動提交模式下(例如,沒有開始任何事務),連接會在每次操作之后被釋放。
關於NHibernate,如果你的應用程序通過使用.NET API,例如System.Transactions 庫,來管理事務,ConnectionReleaseMode.AfterTransaction 模式可能會使NHibernate在一個事務中開啟和關閉多個連接,這會導致一些不必要的開銷和事務從本地事務向分布式事務的升級。將模式制定成ConnectionReleaseMode.OnClose 就能夠阻止這種錯誤。