假設(shè)我們必須處理對象的存儲, 加載, 和查詢. 性能和引用完整性的約束, 給接口的實現(xiàn)帶來了以下問題:
-
加載根對象時如何避免加載大半個數(shù)據(jù)庫
-
存儲時如何更新整個對象圖
-
存儲時如何高效的更新整個對象圖
-
何時同步對象的內(nèi)存狀態(tài)和持久存儲狀態(tài)
-
如何確保在出錯時保持對象內(nèi)存狀態(tài)和持久存儲狀態(tài)之間的一致性
-
如何保證引用的唯一性以避免可能的更新沖突
對性能的精益求精, 又促使人們解決更多的細節(jié)問題:
-
N+1查詢問題
-
分離查詢模型和存儲模型
-
盡量減少查詢語句
這些問題的解決方案又會帶來新的問題.
1. 加載根對象時如何避免加載大半個數(shù)據(jù)庫
更多的時候這是一個建模問題, 為什么我只需要顯示一點信息, 更新一點信息, 卻拉家?guī)Э诎寻藯U子打不著的親戚都帶上 : 細粒度對象設(shè)計, 直接訪問需要的信息, 減少所謂根對象的存在
一個workaround是延遲加載, 當你無法修復(fù)你錯誤的建模時, 當真正去訪問子對象的時候再發(fā)出查詢語句去加載. 這個方案會帶來如下問題:
-
查詢語句較多. 無解, 延遲意味著至少兩條SQL語句, 只能盡量減少
-
延遲加載的時機, 是自動透明的延遲加載, 還是用戶確定何時加載
Hibernate可通過配置文件指定是否lazy load, 一旦指定, 后面的load就是透明的在訪問子對象時發(fā)生. 也可在發(fā)出每次查詢時顯式指定
Entity Framework則要求用戶在每一次查詢時顯式指定包含哪個子對象, 對沒有指定包含的子對象, 只能在訪問前顯示使用load(). 理由是決定加載不加載,何時加載都是程序員的責任
-
然而更大的問題是如何管理數(shù)據(jù)庫連接, 要確保延遲加載的時候數(shù)據(jù)庫連接是開著的
可以使用Interceptor等技術(shù)維持 Session per request, Open Session in View pattern(處理好異常等, 確保session會關(guān)閉).
能在一個 Session 中使用兩個事務(wù)嗎?
是的,這事實上是這種模式(Open Session In View)的一個更好的實現(xiàn)。在一個請求事件中,一個數(shù)據(jù)庫事務(wù)用于數(shù)據(jù)的讀寫。第二個數(shù)據(jù)庫事務(wù)僅用于在渲染視圖期間讀數(shù)據(jù)。在這點上沒有對對象的修改。因此,數(shù)據(jù)庫鎖早在第一個事務(wù)時就被釋放了,這使得應(yīng)用有更好的可伸縮性,第二個事務(wù)可以被優(yōu)化。要使用兩階段的事務(wù),你需要比 Servlet Filter 更強大的攔截器 - AOP 是個很好的選擇。JBoss Seam 使用了這種模式。
為什么 Hibernate 不在需要時就加載 Object?
每個月很多人都會有這種想法,為什么 Hibernate 不能在有需要的就開啟一個新的數(shù)據(jù)庫連接(更有效率的是開啟一個 Session),然后加載集合或是初始化代理,而是選擇拋出一個 LazyInitializationException。當然,這種想法,第一眼看上去可能是明智之舉。但這種做法有很多的缺點,只有當你考慮特別的事務(wù)訪問時才會發(fā)現(xiàn)。
如果 Hibernate 可以進行任意的數(shù)據(jù)庫連接和事務(wù),這種操作是開發(fā)人員不可知,并且也是在任何事務(wù)邊界之外的,那還要事務(wù)邊界做什么。當 Hibernate 開啟了新的數(shù)據(jù)庫連接去加載集合,但同時集合的擁有者卻被刪除了,這是將會發(fā)生什么?(注意,這種情況是不會發(fā)生在上面提到的兩階段的事務(wù)模式中的 - 單個 Session 可對實體可重復(fù)讀。)當所有的對象都可以通過關(guān)聯(lián)導(dǎo)航獲取時為什么還要有 Service 層?這種方式將消耗多少內(nèi)存?哪些對象要首先被清除掉?所有這些問題都是無解的,因為 Hibernate 是一個在線的事務(wù)處理服務(wù)(并包含一些批處理操作),并不是一個“在未定義的工作單元中從數(shù)據(jù)持久倉庫取得對象”的服務(wù)。此外,對于 n+1 查詢問題,我們是否需要 n+1 的事務(wù)和連接的問題?
這個問題的解決方案當然是正確的工作單元劃分和設(shè)計,支撐其的攔截技術(shù)就像這里所展現(xiàn)的一樣,并且/或者正確的抓取技術(shù),使得特定工作單元所需的全部信息能夠以最小的影響、最好的性能和伸縮性被獲得。
2. 存儲時如何更新整個對象圖
框架支持級聯(lián)更新. 是否應(yīng)該級聯(lián)更新, 哪些操作可以級聯(lián), 哪些不可以, 對象之間的哪些類型的關(guān)聯(lián)可以級聯(lián), 哪些不可以, 則是程序員的責任
-
通常被聚合的對象, 其生命周期應(yīng)由父對象負責, 新增/更新/刪除都應(yīng)級聯(lián)
-
自身有存在意義的實體, 可以級聯(lián)更新, 但不應(yīng)刪除和新增
3. 存儲時如何高效的更新整個對象圖
常用工作單元模式, Unit of Work.
4. 何時同步對象的內(nèi)存狀態(tài)和持久存儲狀態(tài)
任何改動都立即提交到數(shù)據(jù)庫會帶來額外開銷. 一個時機是事務(wù)提交時.
Hibernate: 每間隔一段時間,Session會執(zhí)行一些必需的SQL語句來把內(nèi)存中的對象的狀態(tài)同步到JDBC連接中。這個過程被稱為刷出(flush),默認會在下面的時間點執(zhí)行:
-
在某些查詢執(zhí)行之前
-
在調(diào)用org.hibernate.Transaction.commit()的時候
-
在調(diào)用Session.flush()的時候
5. 如何確保在出錯時保持對象內(nèi)存狀態(tài)和持久存儲狀態(tài)之間的一致性
數(shù)據(jù)庫事務(wù)回滾, 清空內(nèi)存緩存, 重新加載
6. 如何避免或處理可能的更新沖突
保證引用的唯一性: 使用單一的加載入口和緩存, Identity Map.
樂觀離線鎖會引入更新沖突問題, 一般使用Versioning來解決, 類似版本控制系統(tǒng)的更新問題; 但業(yè)務(wù)對象很少能自動Merge, Merge的語義也不好定義, 所以一般檢測到?jīng)_突之后只好重做了, 或者取決于業(yè)務(wù)邏輯, Last Win也是一種策略.
7. N+1查詢問題
-
Eager Load + JOIN
-
截然不同的一種避免N+1次查詢的方法是,使用二級緩存。
N + 1 是關(guān)聯(lián)引入的問題, 網(wǎng)上的解釋和例子傾向于拿one-2-many說事, 但實際上one-2-one依然面臨使用多于一條SQL語句加載的問題
8. 分離查詢模型和存儲模型
適合業(yè)務(wù)關(guān)系的對象模型未必對查詢是高效的. 需要單獨針對查詢建模, 可以用單獨的索引表來實現(xiàn). 在更新業(yè)務(wù)對象的存儲時同時更新索引表
9. 盡量減少查詢語句
比如join over multiple select, 比如批量抓取
10. 值類型
不需要有ID, 通常被聚合. 有對應(yīng)的Class, 但一般沒有對應(yīng)的Table, 僅是Table中的幾個字段
挑戰(zhàn)在于將對象語言類型系統(tǒng)(和開發(fā)者定義的實體和值類型)映射到 SQL/數(shù)據(jù)庫類型系統(tǒng)。 Hibernate: 提供了連接兩個系統(tǒng)之間的橋梁:對于實體類型,我們使用class, subclass 等等。對于值類型,我們使用 property, component 及其他,通常跟隨著type屬性。這個屬性的值是Hibernate 的映射類型的名字