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

posted @ 2020-04-25 17:06  子月生  阅读(1289)  评论(2编辑  收藏  举报