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


簡介

這是 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 的“彩蛋”,后續發現其他有趣的地方還會繼續補充,也歡迎大家指正不足的地方。

最后,感謝閱讀。

參考資料

Mybatis官方中文文檔

2021-10-02更改

相關源碼請移步:mybatis-demo

本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/12773971.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM