簡介
這是 Mybatis 系列博客的第四篇,我本來打算詳細講解 mybatis 的配置、映射器、動態 sql 等,但Mybatis官方中文文檔對這部分內容的介紹已經足夠詳細了,有需要的可以直接參考。所以,我將擴展一些其他特性或使用細節,掌握它們可以更優雅、高效地使用 mybatis。
這里補充一點,本文的所有測試例子都是基於本系列 Mybatis 第一篇文章的項目,相關博客如下:
Mybatis源碼詳解系列(一)--持久層框架解決了什么及如何使用Mybatis
Mybatis源碼詳解系列(二)--Mybatis如何加載配置及初始化
Mybatis源碼詳解系列(三)--從Mapper接口開始看Mybatis的執行邏輯
結果處理器--ResultHandler
在分析源碼時,我們提到過 ResultHandler 這個接口,當 Mapper 接口的入參列表中包含 ResultHandler 且返回類型為 void 時(如下代碼),mybatis 會這樣處理:每映射完一個 Employee 對象,都會將這個 Employee 對象傳入 ResultHandler 中。也就是說,通過 ResultHandler,我們可以對傳進來的 Employee 對象進行任意處理。
void queryByCondition(@Param("con") EmployeeCondition condition, ResultHandler<Employee> resultHandler);
這里我提供了一個 ResultHandler 實現類的示例:
public class EmployeeVOResultHandler implements ResultHandler<Employee> {
private List<EmployeeVO> VOs = new ArrayList<>();
@Override
public void handleResult(ResultContext<? extends Employee> resultContext) {
// 獲取Employee
Employee employee = resultContext.getResultObject();
// 轉換為EmployeeVO
EmployeeVO employeeVO = BeanUtils.copy(employeeVO, EmployeeVO.class);
// 添加到結果列表
VOs.add(employeeVO);
}
public List<EmployeeVO> getResults() {
return VOs;
}
}
我認為,官方之所以提供 ResultHandler,是考慮到 resultMap 也有映射不了的對象而做的補充。所以,當我們遇到 resultMap 也映射不了的對象時,可以考慮使用 ResultHandler。
分頁不需要插件--RowBounds
在使用篇中,我們使用 pagehelper 來支持分頁功能,其實,mybatis 已經自帶了分頁功能。和 ResultHandler 一樣,我們只需要改造下 Mapper 接口,如下:
List<Employee> queryByCondition(@Param("con") EmployeeCondition condition, RowBounds rowBounds);
RowBounds 中指定好結果的范圍,mybatis 就會幫我們自動分頁。
@Test
public void testRowBounds() {
// 創建RowBounds
RowBounds rowBounds = new RowBounds(0, 3);
// 構建條件
EmployeeCondition con = new EmployeeCondition();
con.setDeleted(0);
// 執行查詢
List<Employee> employees = employeeMapper.queryByCondition(con, rowBounds);
// 測試
assertTrue(!CollectionUtils.isEmpty(employees));
employees.stream().forEach(System.err::println);
}
相比使用插件,這種方式不是更簡單嗎?那么,我為什么還要使用插件呢?其實,當我們看到控制台打印的 sql,就應該知道原因了(注意,使用 RowBounds 時記得把分頁插件的配置注釋掉):
==> Preparing: 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.deleted = ?
==> Parameters: 0(Integer)
Employee [id=2e18f6560b25473480af987141eccd02, name=zzs005, gender=1, no=zzs005, password=admin, phone=18826****41, address=廣東, status=1, deleted=0, departmentId=94e2d2e56cd811ea802000fffc35d9fa, gmtCreate=Sat Mar 28 00:00:00 CST 2020, gmtModified=Sat Mar 28 00:00:00 CST 2020]
Employee [id=cc6b08506cdb11ea802000fffc35d9fa, name=zzs001, gender=1, no=zzs001, password=666666, phone=18826****42, address=北京, status=1, deleted=0, departmentId=65684a126cd811ea802000fffc35d9fa, gmtCreate=Wed Sep 04 21:48:28 CST 2019, gmtModified=Wed Mar 25 10:44:51 CST 2020]
Employee [id=cc6b08506cdb11ea802000fffc35d9fb, name=zzs002, gender=1, no=zzs002, password=123456, phone=18826****43, address=廣東, status=1, deleted=0, departmentId=65684a126cd811ea802000fffc35d9fa, gmtCreate=Thu Aug 01 21:49:43 CST 2019, gmtModified=Mon Sep 02 21:49:49 CST 2019]
我們發現,mybatis 使用 RowBounds 進行分頁,本質上是把所有數據查出來,再放到應用內存里分頁,這種方式非常耗性能和內存。所以,RowBounds 了解一下就可以了,實際項目中分頁還是考慮引入插件吧。
延遲加載
延遲加載失效了嗎
在使用篇中,當使用嵌套 select 查詢時,我們通過配置 lazyLoadingEnabled 來開啟延遲加載。
<settings>
<setting name="lazyLoadingEnabled" value="true" />
</settings>
按理來說,只有我調用了 getDepartment 時,才會觸發查詢部門的操作,不調用的話永遠都不會觸發。而實際上真的是這樣嗎?下面的方法中,我們把 getDepartment 注釋掉。
@Test
public void testLazyLoading() throws Exception {
// 先查員工
Employee employee = employeeMapper.queryById("cc6b08506cdb11ea802000fffc35d9fe");
// 獲取部門
// Department department = employee.getDepartment();
// 測試
System.err.println(employee);
}
運行測試,控制台中竟然打印了查詢部門的 sql。說好的延遲加載呢?
==> Preparing: select e.* from demo_employee e where e.id = ?
==> Parameters: cc6b08506cdb11ea802000fffc35d9fe(String)
<== Total: 1
==> Preparing: select r.* from demo_role r, demo_employee_role er where er.role_id = r.id and er.employee_id = ? and r.deleted = 0
==> Parameters: cc6b08506cdb11ea802000fffc35d9fe(String)
<== Total: 2
==> Preparing: select d.* from demo_department d where d.id = ?
==> Parameters: 65684a126cd811ea802000fffc35d9fa(String)
<== Total: 1
Employee [id=cc6b08506cdb11ea802000fffc35d9fe, name=zzf001, gender=0, no=zzf001, password=123456, phone=18826****41, address=北京, status=1, deleted=0, departmentId=65684a126cd811ea802000fffc35d9fa, gmtCreate=Wed Sep 04 21:54:49 CST 2019, gmtModified=Wed Sep 04 21:54:51 CST 2019]
什么時候觸發延遲加載
在上面的例子中,難道延遲加載失效了嗎?
其實並沒有,根本原因就在於,mybatis 認為,toString 里會用到 department 字段,所以觸發了查詢部門。同理,equals、clone、hashCode 也是一樣。而事實上,我們項目中使用的 Employee 的 toString 方法並沒有用到 department。
那么,我們如何改變這種行為呢?mybatis 提供了以下配置項:
| 配置項名 | 描述 | 有效值 | 默認值 |
|---|---|---|---|
| lazyLoadTriggerMethods | 指定哪些方法觸發加載該對象的所有延遲加載屬性 | 用逗號分隔的方法列表 | equals,clone,hashCode,toString |
| aggressiveLazyLoading | 開啟時,幾乎任一方法的調用都會加載該對象的所有延遲加載屬性。 否則,每個延遲加載屬性會按需加載。 |
true | false | false (在 3.4.1 及之前的版本中默認為 true) |
我們將配置修改如下:
<settings>
<setting name="lazyLoadingEnabled" value="true" />
<setting name="aggressiveLazyLoading" value="false" />
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode" />
</settings>
再次測試上面的例子。這時,嵌套對象就沒有被加載出來了。
==> Preparing: select e.* from demo_employee e where e.id = ?
==> Parameters: cc6b08506cdb11ea802000fffc35d9fe(String)
<== Total: 1
Employee [id=cc6b08506cdb11ea802000fffc35d9fe, name=zzf001, gender=0, no=zzf001, password=123456, phone=18826****41, address=北京, status=1, deleted=0, departmentId=65684a126cd811ea802000fffc35d9fa, gmtCreate=Wed Sep 04 21:54:49 CST 2019, gmtModified=Wed Sep 04 21:54:51 CST 2019]
作為延遲加載部分的總結,這里對比下不同配置項組合的效果:
| aggressiveLazyLoading | lazyLoadTriggerMethods | 效果 |
|---|---|---|
| true | / | 員工類中任一方法、equals、clone、hashCode、toString被調用,會觸發延遲加 |
| false | equals,clone,hashCode,toString | 員工類中關聯對象的getter方法、equals、clone、hashCode、toString被調用,會觸發延遲加載 |
| false | equals | 員工類中關聯對象的getter方法、equals被調用,會觸發延遲加載 |
有的延遲?有的不延遲
如果我希望部分關聯對象不用延遲加載,部分關聯對象又需要,例如,查詢員工對象時,部門跟着查出來,而角色等到需要用的時候再加載。針對這種情況,可以使用 fetchType 來覆蓋全局配置:
<resultMap id = "EmployResultMap" type = "cn.zzs.mybatis.entity.Employee">
<result column="id" property="id"/>
<result column="department_id" property="departmentId"/>
<association
fetchType="eager"
property="department"
column="department_id"
select="cn.zzs.mybatis.mapper.DepartmentMapper.queryById"/>
<collection
property="roles"
column="id"
select="cn.zzs.mybatis.mapper.RoleMapper.queryByEmployeeId"
/>
</resultMap>
自動映射
mybatis 的結果集自動映射默認是開啟的,可以使用 setting 配置項進行修改:
具體配置方式如下:
<settings>
<setting name="autoMappingBehavior" value="PARTIAL"/>
</settings>
它有三種自動映射等級:
NONE- 禁用自動映射。也就是說你需要在 resultMap 中把所有字段都羅列出來。FULL- 自動映射所有屬性。這種等級配合 mapUnderscoreToCamelCase,幾乎可以自動完成所有字段的映射。PARTIAL- 通常情況下和 FULL 一樣。但是,如果你的 resultMap 中存在嵌套結果集映射,那么,mybatis 只會自動映射嵌套結果集里的字段,而外層的字段就不管了,你需要手動一個個地映射。
<resultMap id = "EmployResultMap2" type = "cn.zzs.mybatis.entity.Employee">
<association property="department">
<result column="departmentName" property="name"/>
<result column="departmentNo" property="no"/>
</association>
</resultMap>
<select id = "queryById2" parameterType = "string" resultMap = "EmployResultMap2">
select e.*, d.name as departmentName, d.no as departmentNo
from demo_employee e left join demo_department d on e.department_id = d.id
where e.id = #{value}
</select>
上面的 xml 中,如果你的自動映射等級為 PARTIAL,則會出現這樣的結果:
==> Preparing: select e.*, d.name as departmentName, d.no as departmentNo from demo_employee e left join demo_department d on e.department_id = d.id where e.id = ?
==> Parameters: cc6b08506cdb11ea802000fffc35d9fe(String)
<== Total: 1
Employee [id=null, name=null, gender=null, no=null, password=null, phone=null, address=null, status=null, deleted=null, departmentId=null, gmtCreate=null, gmtModified=null]
所以,我強烈建議自動映射等級配置為 FULL。
另外, 無論是否在全局開啟了自動映射,你都可以通過 autoMapping 屬性進行覆蓋。
<resultMap id="BaseResultMap2" type="Employee" autoMapping="true">
</resultMap>
結語
以上補充了一些 mybatis 的“彩蛋”,后續發現其他有趣的地方還會繼續補充,也歡迎大家指正不足的地方。
最后,感謝閱讀。
參考資料
2021-10-02更改
相關源碼請移步:mybatis-demo
本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/12773971.html
