iBATIS高級查詢技術詳解

iBATIS In Action爲iBATIS展現自己強大功能提供了保證,iBATIS也可以完成更爲複雜的任務。在本章中,我們會了解新的技術,減少我們的編碼量;以及改善性能、降低資源消耗(footprint)的幾種方法。

1 使用iBATIS操作XML

譯者注:iBATIS的Java版本可以操作基於XML的數據。但意義並不是很大,在以後的版本中該特性可能會被移除。iBATIS.NET則未提供該功能。

2 使用映射語句關聯對象

iBATIS框架也提供了多種方法用以關聯複雜的對象,比如訂單(order)和它的訂單項(order item)(還有它們的相關產品、顧客等等)。每種方法都有其優點和缺點,正所謂“尺有所短,寸有所長”,每一種方案都不是完美的。應根據需要來選擇適合的方案。

注意:爲簡短起見,在本章的餘下的例子中,我們將省略那些對於演示來說不必要的數據。例如,當我們獲取了一個顧客(customer)對象,我們不會獲取它的所有字段,而是僅僅獲取它的主鍵和外鍵。

2.1 複雜的集合屬性

在第4章中,我們學習瞭如何使用SELECT語句從數據庫獲取數據。在那些例子中,我們獲取的結果僅僅是單個對象,即使是連接多表也是如此。事實上,如果您有多個複雜對象,也可以使用iBATIS加載它們。

如果我們的應用程序模型與數據模型比較類似,那麼這個功能會很有用。可以考慮根據對象的關係(關聯)來定義數據模型,然後使用iBATIS將它們一起加載。例如,如果在數據庫中,Account記錄對應着相關的Order記錄,而Order又對應着相關的OrderItem記錄,可以爲這些記錄建立關係,當我們請求一條Account記錄時,可以一併獲取所有的Order和OrderItem記錄。下面的代碼清單顯示瞭如何定義我們的SQL映射:

SQL映射 

圖1SQL映射

先來看看結果映射(result map,即上面的ResultAccountInfoMap,ResultOrderInfoMap和ResultOrderItemMap),前兩個Map都用到了select特性。這個特性告訴iBATIS,屬性的值將由另一個映射語句來設置,語句的名稱就是select特性的值。例如,我們執行getAccountInfoList語句時,ResultAccountInfoMap結果映射有一個子元素:

select特性的值 

圖2select特性的值

它的作用是告訴iBATIS,account對象的orderList屬性的值由ChgetOrderInfoList語句來設置,同時把accountId列的值傳給ChgetOrderInfoList作爲參數。類似地,在設置order對象的orderItemList對象時,也會執行getOrderItemList語句。

這個功能給我們帶來便利的同時,也帶來了兩個問題。首先,創建包含大量對象的列表可能會消耗大量的內存。其次,這種方法會導致數據庫的I/O問題,其原因是所謂的“N+1 Select”現象,這個現象將在後面討論。對於每個問題,iBATIS框架都提供瞭解決方案,但是注意,沒有哪一種能同時解決這兩個問題。

數據庫I/O

數據庫I/O是數據庫使用狀況的一項指標,也是數據庫性能的主要瓶頸之一。在讀取或寫入數據庫時,數據必須要經歷從磁盤到內存或者從內存到磁盤的轉換,這個過程是比較耗時的。在程序中使用緩存可以減少對數據庫的訪問,但這種方法使用時要謹慎,否則也會引發問題。要了解iBATIS中的緩存機制,可以參看第10章的內容。

在使用關聯數據時,可能會遭遇數據庫I/O問題。考慮一下這個場景:有1000個Account,每一個關聯了1000個Order,而每個Order則包含25個OrderItem。如果嘗試將所有這些數據加載到內存,執行的SQL語句要超過1000000行(1條用來查詢Account,1000條用於Order,1000000條用於OrderItem),而創建的對象大約爲2500萬——如果你真敢這麼做,等你的系統管理員收拾你吧。

分析N+1查詢問題

N+1查詢問題是由於試圖加載多個父記錄(比如Account)的子記錄(Order)而引起的。因此,在查詢父記錄時,只需要1條語句,假設返回N條記錄,那麼就需要再執行N條語句來查詢子記錄,引發所謂的“N+1查詢”。

這些問題的解決方案

延遲加載(Lazy load,在2.2中詳細講述)可以解決一部分內存問題,它將加載過程打散爲一些更小的過程。但是,它並沒有解決數據庫I/O問題,在最壞的情況下,它對數據庫的訪問次數與非延遲加載的版本是一樣的,因爲加載數據時它的方法還是N+1查詢(這個我們將在2.3中解決)。另一方面,當我們解決了N+1查詢問題,減少了對數據庫的訪問,但我們的查詢結果卻包含着2500萬行記錄!

要決定是否使用複雜屬性,我們需要理解數據庫以及應用程序對數據庫的使用方式。如果您使用了本節中的技術,那可以省不少事兒,但如果誤用了它,也會有大麻煩。在接下來的兩節中,我們會分析如何根據目標選擇合適的策略。

讓我們從這個問題開始:像上面例子那樣將Account關聯到Order並將Order關聯到OrderItem是否合適?實際上,不是——order-to-orderitem關係是固定的,但是account-to-order關係則是不必要的。

我們是如是推理的:沒有所屬的Order,OrderItem是沒有意義的,而Account則是有意義的。一般情況下,沒有OrderItem,Order沒什麼大用,相對的,不屬於任何Order的OrderItem是沒有意義的。另一方面,一個Account則可以認爲是一個完整的對象。

但在我們的例子中,這種關係可以良好地描述相關的技術,因此我們會在一段時間內一直使用它。

2.2 延遲加載(Lazy loading)

首先來看看延遲加載。如果不是對所有數據都馬上用到,那麼延遲加載是有用的。例如,我們的程序首先在一個網頁顯示所有Account,然後銷售代理(我們的客戶)可以點擊一個Account來查看該Account的Order列表,然後可以再點擊一個Order來查看其所有的OrderItem信息。在這種情況下,每次都僅查詢一個列表。這是對延遲加載的合理使用。

譯註:在iBATIS的Java版本中,使用延遲加載前還需要進行配置SqlMapConfig.xml以打開該功能。在.NET版本中不需要配置等價的sqlMap.config。

使用了延遲加載後,我們就可以更合理地進行對象創建和對數據庫的訪問。(還是使用上面的例子)如果一個用戶關注到OrderItem層次的數據,我們需要進行三次查詢(一次是爲Account,一次是Order,還有一次是OrderItem),應用程序則要創建2025個對象(1000個Account, 1000個Order,25個OrderItem)。效果明顯!而我們要做的僅僅是修改XML配置文件的一個特性(attribute)而已,無需改動代碼。

在一項不太嚴謹的測試中,我們發現,對於同樣的對象關聯關係(如上面的Account- Order- ORderItem),在加載第一個列表數據時(Account列表),沒有使用延遲加載的版本花費的時間是使用了延遲加載的版本的三倍。但是,在加載所有數據時,延遲加載的版本的時間卻多了20%。很明顯,我們要根據數據加載的數量和時機來確定是否採用延遲加載。此時,經驗是最重要的。

而有時您並不希望推遲數據的加載,而是希望在第一次請求的時候加載所有的數據。在這種情況下,您可以使用下節中的技術,它僅需要一次查詢即可。下節的方法避免了“N+1查詢”。

2.3 避免“N+1查詢”問題

我們來考慮如何避免“N+1查詢”問題,這裏可以使用連接語句(Join)。

這裏用到的技術同前面類似。簡單的說,使用Result Map來定義對象間的關係,將頂層的Result Map關聯到映射語句。下面的例子的Data Map文件結構與前面大體一致,但是隻需要執行一條SQL語句。

這裏面有三個Result Map,一是關於Account的,二是關於Order的,三是OrderItem的。

關於Account的Result Map有兩個作用:

映射Account對象本身的屬性。

告訴iBATIS如何映射下一層的關聯對象,這裏是orderList。

Order的Result Map作用與之類似。

映射Order對象本身的屬性。

告訴iBATIS如何映射下一層的關聯對象,這裏是orderItemList。

orderItemList 

圖3orderItemList

我們的不太科學的測試表明,在加載少量數據時,該方法將原先方法的性能提高爲7:1。我們猜想,對於例子中使用的2500萬條數據,兩種方法仍然不錯。

譯註:在iBATIS.NET DataMapper 1.1中,添加了groupBy特性,它將進一步改善性能。詳細內容請參看相關文檔,本文使用的是DataMapper 1.5.1。

需要注意的是,儘管性能得到改善,內存的消耗仍然與沒有使用延遲加載的版本相同。所有的記錄一起放入內存,因此儘管它稍微快了一點,但內存的消耗仍是問題。

譯註:在加載複雜屬性時可能出現兩方面的問題,一是對數據庫的訪問,二是創建對象時對內存的消耗。我們可以採用延遲加載或Join的方法來解決這些問題,但是兩者都不是萬靈藥。延遲加載的原理時推遲對複雜屬性的加載,以減少對數據庫的訪問和對象的創建,但它的前提是複雜屬性不會馬上用到,否則的話,延遲就失去意義。Join的原理是通過一條SQL語句加載所有數據,這樣可以大幅度減少對數據庫的訪問量,它的前提是對象的數量不會太多。該如何選擇呢?下面的表格給出了簡單的原則:

簡單的原則 

圖4簡單的原則

譯註:另外,我覺得還有一條很重要的原則,那就是永遠只加載必需的數據。以上面的例子來說,我們不太可能會同時顯示1000個Account給用戶看,這時就不要同時加載1000個Account的數據了,可以通過分頁只顯示50條數據,在此基礎上再應用延遲加載或Join效果會很不錯。關於在Web項目中如何使用iBATIS進行分頁,請參看這篇文章。

關於iBATIS使用的相關以及複雜屬性映射相關的內容就介紹到這裏,希望對你有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章