強大的結果處理器ResultHandler
DO轉VO--常用方式
通常情況下,我們的持久層的對象不會(不應該)直接響應給調用者,需要轉換為 VO 對象再響應出去。基於本系列博客的使用例子,這裡假設我需要在 web 層返回下面的 VO 對象,如下。在這個類中,除了員工表的欄位外,還包括了部門表的欄位。
web 層的操作大致是這樣的,我先查詢出Employee的集合,然後再進行對象轉換。
DO轉VO--ResultHandler方式
使用 Mybatis 的話,其實還有另外一種方案來處理 DO 轉 VO 的問題,就是採用結果處理器--ResultHandler,如下。
這是一個接口,實現類需要我們自己定義。作為測試例子,這裡我簡單定義了一個。
使用ResultHandler時,Mapper 接口的方法定義需要調整,入參需傳入ResultHandler,且返回值必須為 void。至於 xml 對應的方法內容,還是和常用方式一樣,不需要更改。下面兩個方法共用一個 xml 的 select 節點不會出問題的,這一點不用擔心。
最後回到我們的 web 層,至於 service 層的代碼就忽略不看了。當調用 service 層時,我已經拿到了轉換好的 VO 對象,我不需要再做處理。
當 Mapper 接口的方法入參包含ResultHandler且返回類型為 void,Mybatis 會對這種情況特殊處理:當遍歷結果集進行映射時,每映射完一個對象都會調用一次ResultHandler並將映射好的對象傳入,這時,我們可以隨意地對對象進行處理,包括我們常見的 DO 轉 VO,當然,它的功能並不局限於此。
分頁不需要插件RowBounds
本系列使用篇中提到使用 pagehelper 來支持分頁功能,本質上是使用了插件對 sql 植入分頁參數。其實,Mybatis 已經提供了RowBounds這類來支持分頁功能,這種方式不需要安裝插件,MybatisPlus 本質上就是使用了這種方式。
和ResultHandler一樣,我們只需要改造下 Mapper 接口的方法,如下。
List<Employee> selectByCondition(@Param("con") EmployeeCondition con, RowBounds rowBounds);
這裡我簡單編寫個測試類,直接使用RowBounds對象,實際上最好對RowBounds進行更多的包裝。
測試以上代碼,可看到打出的語句植入了分頁參數:
相比使用插件,這種方式是否更加簡單呢?
延遲加載
我們知道,在 resultMap 中使用嵌套 select 查詢,並且全局聲明使用懶加載,可以實現按需加載嵌套屬性。
還是回到使用篇中例子,mapper 的配置如下,員工對象關聯了部門(一對一)、角色(一對多)、菜單(一對多):
測試代碼中,我們注釋掉第1、3 和 4 點的代碼,即只調用getDepartment()方法。
測試以上代碼,可以看到,只有部門被加載出來,而角色和菜單沒有,很好地實現了按需加載。
接著我們放開第 1 點,即增加列印員工,注意,使用例子中我並沒有重寫toString()方法,所以方法中也不會用到關聯對象。
測試以上代碼,我們驚訝地發現,這時部門、角色和菜單都被列印出來了,說好的按需加載呢?
這就很奇怪了,我調用的方法並沒用到關聯對象啊,為什麼它們會被加載出來?
什麼時候觸發延遲加載
在上面的例子中,我們的按需加載失效了嗎?
其實並沒有,對於 Mybatis 而言,它可以知道getDepartment()這樣的方法會使用到關聯對象,但是toString()這樣的方法,它就沒辦法知道了。考慮我們會在重寫toString方法時使用到嵌套對象,所以,Mybatis 默認這個方法會觸發延遲加載。同樣道理,equals(),clone(),hashCode()等方法也是一樣的,項目中要重點關注equals()和hashCode()。
那麼,我們如何控制這種行為呢?Mybatis 提供了 lazyLoadTriggerMethods 配置項指定對象的哪些方法觸發延遲加載:
我們將配置修改如下:
<settingname="lazyLoadingEnabled"value="true" /><settingname="lazyLoadTriggerMethods"value="equals,clone,hashCode" />
再次測試上面的例子。這時,嵌套對象都沒有被加載出來。
這裡再補充下,還有另一個配置項 aggressiveLazyLoading 也會影響延遲加載的觸發,這個配置項在 3.4.1 之後我們保持默認就行,如果不是必須,強烈建議不要配置成 true。如果你將 aggressiveLazyLoading 配置為 true,即使你只是 getId() 也會將所有嵌套對象加載出來。
作為延遲加載部分的總結,這裡對比下不同配置項組合的效果:
有的延遲?有的不延遲
如果我希望部分關聯對象不用延遲加載,部分關聯對象又需要,例如,查詢員工對象時,部門跟著查出來,而角色等到需要用的時候再加載。針對這種情況,可以在映射關係中使用 fetchType來覆蓋延遲加載的開關狀態:
嵌套結果映射的一個大坑
嵌套結果裡如果是collection的話,分頁總數會存在問題,所以,嵌套結果映射的方式最好僅針對 association 使用。
當時我沒有解釋具體原因,這裡我補充下吧。
錯誤的總數
mapper 的 resultMap 是這樣配置示例:
編寫測試方法如下。這裡會採用分頁插件 pagehelper 來統計查詢總數,及進行分頁。如果使用RowBounds,也不影響測試結果。注意,資料庫中的「zzs001」只有一條記錄,所查詢到的總數和映射對象都會是一條。
測試代碼,可以看到分頁統計的總數和實際數量都會是一條,完全沒問題。
接下來我再 resultMap 中增加一個 collection 類型的嵌套對象。
放開測試代碼中的注釋,測試如下。映射對象一條,沒錯,但是查詢總數,竟然是 2 條???
這就是我提到的嵌套結果映射的一個大坑。
原因分析
難道是統計錯了?讓我們執行下控制臺的 sql,記錄竟然也是 2 條,哪裡冒出來的???
其實,根本原因確實出在我們的使用方法上,collection 的嵌套結果映射就不應該被用在涉及到統計的場景。我們的 sql 查出來有兩條,仔細觀察就會發現,這兩條記錄的 id 是一模一樣的,我們再查詢出 1 個欄位:
看到這裡應該就明白了吧,統計出錯主要是聯表造成的。員工和角色是一對多的關係,當員工擁有多個角色時聯表查詢將出現比員工數量更多的記錄,而這些記錄,在 Mybatis 映射對象時會將其合併起來。
這就造成了所謂的錯誤總數問題。所以,collection 的嵌套結果映射並不適合統計場景。
自動映射
開啟自動映射
mybatis 的結果自動映射默認是開啟的,可以在使用 setting 配置項進行修改,它有三種自動映射等級:
NONE - 禁用自動映射。僅對手動映射的屬性進行映射。PARTIAL - 對除在內部定義了嵌套結果映射(也就是連接的屬性)以外的屬性進行映射。默認配置。FULL - 自動映射所有屬性。默認使用 PARTIAL,另外, 無論設置的自動映射等級是哪種,你都可以通過在映射文件中設置 resultMap 的 autoMapping 屬性來為指定的結果映射設置啟用/禁用自動映射。
自動映射駝峰命名的屬性
當自動映射查詢結果時,MyBatis 會獲取結果中返回的列名並在 Java 類中查找相同名字的屬性(忽略大小寫)。如果列名和實體中的屬性名對不上,則需要顯式地配置。在使用例子中,我們使用resultMap來映射表和對象,如下:
除了表列名和實體的屬性名一致的情況,其他的欄位都需要我們手動配置映射,這樣做比較麻煩。但是,大部分情況下,我們都會遵循駝峰命名的規則來定義實體的屬性名,是否可以直接通過這種規則來自動映射呢?
mybatis 提供了mapUnderscoreToCamelCase配置項來處理這種情況。