1. 程式人生 > >Mybatis原始碼詳解系列(四)--你不知道的Mybatis用法和細節

Mybatis原始碼詳解系列(四)--你不知道的Mybatis用法和細節

# 簡介 這是 [Mybatis](https://www.cnblogs.com/ZhangZiSheng001/p/12603885.html) 系列部落格的第四篇,我本來打算詳細講解 mybatis 的配置、對映器、動態 sql 等,但[Mybatis官方中文文件](https://Mybatis.org/Mybatis-3/zh/index.html/)對這部分內容的介紹已經足夠詳細了,有需要的可以直接參考。所以,我將擴充套件一些其他特性或使用細節,掌握它們可以更優雅、高效地使用 mybatis。 這裡補充一點,本文的所有測試例子都是基於本系列 [Mybatis](https://www.cnblogs.com/ZhangZiSheng001/p/12603885.html) 第一篇文章的專案,其他相關部落格如下: [Mybatis原始碼詳解系列(一)--持久層框架解決了什麼及如何使用Mybatis](https://www.cnblogs.com/ZhangZiSheng001/p/12603885.html) [Mybatis原始碼詳解系列(二)--Mybatis如何載入配置及初始化](https://www.cnblogs.com/ZhangZiSheng001/p/12704076.html) [Mybatis原始碼詳解系列(三)--從Mapper介面開始看Mybatis的執行邏輯](https://www.cnblogs.com/ZhangZiSheng001/p/12761376.html) # 強大的結果處理器--ResultHandler ## DO轉VO--常用方式 通常情況下,我們的持久層的物件不會(不應該)直接響應給呼叫者,需要轉換為 VO 物件再響應出去。基於本系列部落格的使用例子,這裡假設我需要在 web 層返回下面的 VO 物件,如下。在這個類中,除了員工表的欄位外,還包括了部門表的欄位。 ```java public class EmployeeVO implements Converter, Serializable { private static final long serialVersionUID = 1L; private String id; private String name; private String genderStr; private String no; private String password; private String phone; private String address; private Byte status; private String departmentId; private String departmentName; private String departmentNo; @Override public EmployeeVO convert(Employee value) { EmployeeVO employeeVO = new EmployeeVO(); BeanUtils.copyProperties(employeeVO, value); employeeVO.setGenderStr(value.getGender()?"男":"女"); Department department = value.getDepartment(); if(department != null) { employeeVO.setDepartmentName(department.getName()); employeeVO.setDepartmentNo(department.getNo()); } return employeeVO; } // 省略其他方法 } ``` web 層的操作大致是這樣的,我先查詢出`Employee`的集合,然後再進行物件轉換。 ```java @RequestMapping("/getList") public ResponseData testResultHandler(@RequestBody EmployeeCondition con) { List list = employeeService.list(con); return ResultDataUtil.getResultSucess(ConvertUtil.convertList(list, new EmployeeVO())); } ``` ## DO轉VO--ResultHandler方式 使用 Mybatis 的話,其實還有另外一種方案來處理 DO 轉 VO 的問題,就是採用結果處理器--`ResultHandler`,如下。 ```java public interface ResultHandler { void handleResult(ResultContext resultContext); } ``` 這是一個介面,實現類需要我們自己定義。作為測試例子,這裡我簡單定義了一個。 ```java public class MyResultHandler implements ResultHandler { private List list = new ArrayList(); private Converter converter; public MyResultHandler(Converter converter) { this.converter = converter; } @Override public void handleResult(ResultContext resultContext) { list.add(ConvertUtils.convertObject(resultContext.getResultObject(), converter)); } public List getList(){ return list; } } ``` **使用`ResultHandler`時,Mapper 介面的方法定義需要調整,入參需傳入`ResultHandler`,且返回值必須為 void**。至於 xml 對應的方法內容,還是和常用方式一樣,不需要更改。下面兩個方法共用一個 xml 的 select 節點不會出問題的,這一點不用擔心。 ```java // 常用的方式 List selectByCondition(@Param("con") EmployeeCondition con); // ResultHandler的方式 void selectByCondition(@Param("con") EmployeeCondition con, ResultHandler resultHandler); ``` 最後回到我們的 web 層,至於 service 層的程式碼就忽略不看了。當呼叫 service 層時,我已經拿到了轉換好的 VO 物件,我不需要再做處理。 ```java @RequestMapping("/getList") public ResponseData testResultHandler(@RequestBody EmployeeCondition con) { MyResultHandler resultHandler = new MyResultHandler<>(new EmployeeVO()); employeeService.list(con, resultHandler); return ResultDataUtil.getResultSucess(resultHandler.getList()); } ``` 上一篇部落格在分析原始碼過程中有提到過這個介面,**當 Mapper 介面的方法入參包含`ResultHandler`且返回型別為 void,Mybatis 會對這種情況特殊處理:當遍歷結果集進行對映時,每對映完一個物件都會呼叫一次`ResultHandler`並將對映好的物件傳入,這時,我們可以隨意地對物件進行處理,包括我們常見的 DO 轉 VO,當然,它的功能並不侷限於此**。 # 分頁不需要外掛--RowBounds 本系列使用篇中提到使用 pagehelper 來支援分頁功能,本質上是使用了外掛對 sql 植入分頁引數。其實,Mybatis 已經提供了`RowBounds`這類來支援分頁功能,這種方式不需要安裝外掛,MybatisPlus 本質上就是使用了這種方式。 和`ResultHandler`一樣,我們只需要改造下 Mapper 介面的方法,如下。 ```java List selectByCondition(@Param("con") EmployeeCondition con, RowBounds rowBounds); ``` 這裡我簡單編寫個測試類,直接使用`RowBounds`物件,實際上最好對`RowBounds`進行更多的包裝。 ```java /** *

測試RowBounds

*/ @Test public void testRowBounds() { EmployeeCondition con = new EmployeeCondition(); // 設定條件 con.setAddress("北京"); // 執行,獲取員工物件 RowBounds rowBounds = new RowBounds(1, 4); List list = employeeRepository.list(con, rowBounds); // 列印 list.forEach(System.out::println); } ``` 測試以上程式碼,可看到打出的語句植入了分頁引數: ```sql SELECT e.id, e.`name`, e.gender, e.no, e.password , e.phone, e.address, e.status, e.deleted, e.department_id , e.gmt_create, e.gmt_modified FROM demo_employee e WHERE 1 = 1 AND e.address = ? LIMIT ?, ? ``` 相比使用外掛,這種方式是否更加簡單呢? # 延遲載入 ## 回顧使用篇的內容 我們知道,在 resultMap 中使用巢狀 select 查詢,並且全域性宣告使用懶載入,可以實現按需載入巢狀屬性。 ```xml
``` 還是回到使用篇中例子,mapper 的配置如下,員工物件關聯了部門(一對一)、角色(一對多)、選單(一對多): ```xml ``` 測試程式碼中,我們註釋掉第1、3 和 4 點的程式碼,即只調用`getDepartment()`方法。 ```java /** *

測試懶載入觸發

*/ @Test public void testGetLazy() { // 設定輸出代理類到指定路徑 // -Dcglib.debugLocation=D:/growUp/test String id = "cc6b08506cdb11ea802000fffc35d9fe"; // 執行,獲取員工物件 Employee employee = employeeRepository.get(id); // 1.列印員工 // System.out.println(employee); // 2.列印部門 System.out.println(employee.getDepartment()); // 3.列印角色 // employee.getRoles().forEach(System.out::println); // 4.列印選單 // employee.getMenus().forEach(System.out::println); } ``` 測試以上程式碼,可以看到,只有部門被加載出來,而角色和選單沒有,很好地實現了按需載入。
接著我們放開第 1 點,即增加列印員工,注意,使用例子中我並沒有重寫`toString()`方法,所以方法中也不會用到關聯物件。 ```java @Test public void testGetLazy() { // 設定輸出代理類到指定路徑 // -Dcglib.debugLocation=D:/growUp/test String id = "cc6b08506cdb11ea802000fffc35d9fe"; // 執行,獲取員工物件 Employee employee = employeeRepository.get(id); System.out.println("================"); // 1.列印員工 System.out.println(employee); // 2.列印部門 // System.out.println(employee.getDepartment()); // 3.列印角色 // employee.getRoles().forEach(System.out::println); // 4.列印選單 // employee.getMenus().forEach(System.out::println); } ``` 測試以上程式碼,我們驚訝地發現,這時部門、角色和選單都被打印出來了,說好的按需載入呢? ![mybatis_extend_02](https://img2020.cnblogs.com/blog/1731892/202004/1731892-20200425170151060-1710900558.png) 這就很奇怪了,我呼叫的方法並沒用到關聯物件啊,為什麼它們會被加載出來? ## 什麼時候觸發延遲載入 在上面的例子中,我們的按需載入失效了嗎? 其實並沒有,**對於 Mybatis 而言,它可以知道`getDepartment()`這樣的方法會使用到關聯物件,但是`toString()`這樣的方法,它就沒辦法知道了**。考慮我們會在重寫`toString`方法時使用到巢狀物件,所以,Mybatis 預設這個方法會觸發延遲載入。同樣道理,`equals()`,`clone()`,`hashCode()`等方法也是一樣的,專案中要重點關注`equals()`和`hashCode()`。 那麼,我們如何控制這種行為呢?Mybatis 提供了 lazyLoadTriggerMethods 配置項指定物件的哪些方法觸發延遲載入: | 設定名 | 描述 | 有效值 | 預設值 | | :--------------------- | :--------------------------------------------- | :--------------------- | :----------------------------- | | lazyLoadTriggerMethods | 指定哪些方法觸發載入該物件的所有延遲載入屬性。 | 用逗號分隔的方法列表。 | equals,clone,hashCode,toString | 我們將配置修改如下: ```xml ``` 再次測試上面的例子。這時,巢狀物件都沒有被加載出來。 這裡再補充下,還有另一個配置項 aggressiveLazyLoading 也會影響延遲載入的觸發,這個配置項在 3.4.1 之後我們保持預設就行,如果不是必須,強烈建議不要配置成 true。**如果你將 aggressiveLazyLoading 配置為 true,即使你只是 getId() 也會將所有巢狀物件加載出來**。 | 設定名 | 描述 | 有效值 | 預設值 | | :--------------------- | :----------------------------------------------------------- | :--------------------- | :------------------------------------------- | | aggressiveLazyLoading | 開啟時,幾乎任一方法的呼叫都會載入該物件的所有延遲載入屬性。
否則,每個延遲載入屬性會按需載入。 | true \| false | false (在 3.4.1 及之前的版本中預設為 true) | 作為延遲載入部分的總結,這裡對比下不同配置項組合的效果: | aggressiveLazyLoading | lazyLoadTriggerMethods | 效果 | | :-------------------- | :----------------------------- | :----------------------------------------------------------- | | true | / | 員工類中任一方法、equals、clone、hashCode、toString被呼叫,會觸發延遲加 | | false | equals,clone,hashCode,toString | 員工類中關聯物件的getter方法、equals、clone、hashCode、toString被呼叫,會觸發延遲載入 | | false | equals | 員工類中關聯物件的getter方法、equals被呼叫,會觸發延遲載入 | ## 有的延遲?有的不延遲 如果我希望部分關聯物件不用延遲載入,部分關聯物件又需要,例如,查詢員工物件時,部門跟著查出來,而角色等到需要用的時候再載入。針對這種情況,可以在對映關係中使用 `fetchType`來覆蓋延遲載入的開關狀態: ```xml ``` # 巢狀結果對映的一個大坑 在使用篇裡我說過這麼一句話:**巢狀結果裡如果是`collection`的話,分頁總數會存在問題,所以,巢狀結果對映的方式最好僅針對 association 使用**。 當時我沒有解釋具體原因,這裡我補充下吧。 ## 錯誤的總數 還是回到使用篇的例子,mapper 的 resultMap 是這樣配置的: ```xml ``` 編寫測試方法如下。這裡會採用分頁外掛 pagehelper 來統計查詢總數,及進行分頁。如果使用`RowBounds`,也不影響測試結果。注意,**資料庫中的“zzs001”只有一條記錄,所查詢到的總數和對映物件都會是一條**。 ```java @Test public void testlistPage() { EmployeeCondition con = new EmployeeCondition(); // 設定條件 con.setName("zzs001"); con.setJoinDepartment(true); // con.setJoinRole(true);// 這個註釋待會放開 // 設定分頁資訊 PageHelper.startPage(0, 3); // 執行查詢 List list = employeeRepository.list2(con); // 遍歷結果 list.forEach(System.out::println); // 封裝分頁模型 PageInfo pageInfo = new PageInfo<>(list); // 取分頁模型的資料 System.out.println(Long.valueOf(pageInfo.getTotal()).intValue() == list.size()); } ``` 測試程式碼,可以看到分頁統計的總數和實際數量都會是一條,完全沒問題。 ![mybatis_extend_04](https://img2020.cnblogs.com/blog/1731892/202004/1731892-20200425170244108-1501397730.png) 接下來我再 resultMap 中增加一個 collection 型別的巢狀物件。 ```xml ``` 放開測試程式碼中的註釋,測試如下。對映物件一條,沒錯,但是查詢總數,竟然是 2 條??? 這就是我提到的巢狀結果對映的一個大坑。 ## 原因分析 難道是統計錯了?讓我們執行下控制檯的 sql,記錄竟然也是 2 條,哪裡冒出來的??? 其實,根本原因確實出在我們的使用方法上,**collection 的巢狀結果對映就不應該被用在涉及到統計的場景**。我們的 sql 查出來有兩條,仔細觀察就會發現,這兩條記錄的 id 是一模一樣的,我們再查詢出 1 個欄位: 看到這裡應該就明白了吧,統計出錯主要是聯表造成的。員工和角色是一對多的關係,當員工擁有多個角色時聯表查詢將出現比員工數量更多的記錄,而這些記錄,在 Mybatis 對映物件時會將其合併起來。 這就造成了所謂的錯誤總數問題。所以,collection 的巢狀結果對映並不適合統計場景。 # 自動對映 ## 開啟自動對映 mybatis 的結果自動對映預設是開啟的,可以在使用 setting 配置項進行修改,它有三種自動對映等級: - `NONE` - 禁用自動對映。僅對手動對映的屬性進行對映。 - `PARTIAL` - 對除在內部定義了巢狀結果對映(也就是連線的屬性)以外的屬性進行對映。預設配置。 - `FULL` - 自動對映所有屬性。 預設使用 PARTIAL,另外, 無論設定的自動對映等級是哪種,你都可以通過在對映檔案中設定 resultMap 的 `autoMapping` 屬性來為指定的結果對映設定啟用/禁用自動對映。 ```xml ``` ## 自動對映駝峰命名的屬性 當自動對映查詢結果時,MyBatis 會獲取結果中返回的列名並在 Java 類中查詢相同名字的屬性(忽略大小寫)。如果列名和實體中的屬性名對不上,則需要顯式地配置。在使用例子中,我們使用`resultMap`來對映表和物件,如下: ```xml e.id, e.`name`, e.gender, e.no, e.password, e.phone, e.address, e.status, e.deleted, e.department_id, e.gmt_create, e.gmt_modified ``` 除了表列名和實體的屬性名一致的情況,其他的欄位都需要我們手動配置對映,這樣做比較麻煩。但是,大部分情況下,我們都會遵循駝峰命名的規則來定義實體的屬性名,是否可以直接通過這種規則來自動對映呢? mybatis 提供了`mapUnderscoreToCamelCase`配置項來處理這種情況。 ```xml ``` # 參考資料 [Mybatis官方中文文件](https://Mybatis.org/Mybatis-3/zh/index.html/) > 相關原始碼請移步:[mybatis-demo](https://github.com/ZhangZiSheng001/mybatis-projects/tree/master/mybatis-demo) > 本文為原創文章,轉載請附上原文出處連結:[https://www.cnblogs.com/ZhangZiSheng001/p/12773971.html ]( https://www.cnblogs.com/ZhangZiSheng001/p/12773971.h