假設我們必須處理對象的儲存, 載入, 和查詢. 效能和參考完整性的約束, 給介面的實現帶來了以下問題:
載入根對象時如何避免載入大半個資料庫
儲存時如何更新整個對象圖
儲存時如何高效的更新整個對象圖
何時同步對象的記憶體狀態和持久儲存狀態
如何確保在出錯時保持對象記憶體狀態和持久儲存狀態之間的一致性
如何保證引用的唯一性以避免可能的更新衝突
對效能的精益求精, 又促使人們解決更多的細節問題:
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 的映射類型的名字