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