Mybatis(三) - mapper.xml及其加载机制

1. mapper.xml详解

常见的mapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.linkedbear.mybatis.mapper.UserMapper">
    <resultMap id="userMap" type="com.linkedbear.mybatis.entity.User">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="birthday" column="birthday"/>
        <association property="department" javaType="com.linkedbear.mybatis.entity.Department">
            <id property="id" column="department_id"/>
            <result property="name" column="department_name"/>
        </association>
    </resultMap>

    <resultMap id="userlazy" type="com.linkedbear.mybatis.entity.User">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="birthday" column="birthday"/>
        <association property="department" javaType="com.linkedbear.mybatis.entity.Department"
                     select="com.linkedbear.mybatis.mapper.DepartmentMapper.findById" column="department_id"/>
    </resultMap>

    <select id="findAll" resultMap="userMap">
        select usr.*, dep.name as department_name
        from tbl_user usr
        left join tbl_department dep on usr.department_id = dep.id
    </select>

    <select id="findAllLazy" resultMap="userlazy">
        select * from tbl_user
    </select>

    <insert id="saveUser" parameterType="com.linkedbear.mybatis.entity.User">
        insert into tbl_user (id, name, department_id) VALUES (#{id}, #{name}, #{department.id})
    </insert>

    <select id="findAllByDepartmentId" parameterType="string"
            resultType="com.linkedbear.mybatis.entity.User">
        select * from tbl_user where department_id = #{departmentId}
    </select>
</mapper>

1.1 select - DQL

1.1.1 标签的属性含义

首先我们先看看 select 标签本身的属性,这里面大多数我们还都比较熟悉,

属性描述备注

id

一个 namespace 下的 statement 的唯一标识

不同的 namespace 下可以定义相同的 id

parameterType

执行 statement 传入的参数的类型

该属性可以不填,MyBatis 会根据 TypeHandler 自动推断传入的参数类型

resultType

从执行的 SQL 查询结果集的封装实体类型全限定名或别名

如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型;resultType 和 resultMap 之间只能同时使用一个

resultMap

mapper.xml 中定义的任意 resultMap 的 id 引用

如果引用的 resultMap 是在其他 mapper.xml 中,则引用的 id 为 [命名空间 + '.' + id] ;resultType 和 resultMap 之间只能同时使用一个

useCache

查询结果是否保存至二级缓存

默认 true

flushCache

执行 SQL 后会清空一级缓存(本地缓存)和二级缓存

默认 false ;所有 namespace 的一级缓存和当前 namespace 的二级缓存均会清除【1.2】

timeout

SQL 请求的最大等待时间(单位: 秒)

默认无限制,推荐定义全局最大等待时间( settings → defaultStatementTimeout )

fetchSize

底层数据库驱动一次查询返回的结果行数

无默认值(依赖不同的数据库驱动),该配置与 MyBatis 无关,仅与底层数据库驱动有关【1.3】

statementType

底层使用的 Statement 的类型

可选值:STATEMENT , PREPARED , CALLABLE ,默认 PREPARED ,底层使用 PreparedStatement

resultSetType

控制 jdbc 中 ResultSet 对象的行为

可选值:FORWARD_ONLY , SCROLL_SENSITIVE , SCROLL_INSENSITIVE , DEFAULT【1.4】

databaseId

用于部分不同数据库厂商下使用的 SQL

会加载所有不带 databaseId 的,以及匹配激活的数据源对应的数据库厂商的 databaseId 的 statement

常用的属性小册不作过多解释,下面针对几个可能产生疑问的属性,我们详细的来探究一下。

1.1.2 flushCache

有关 flushCache 到底是清除哪些缓存,如果小册前面不特意说明是所有 namespace 的话,可能会有小伙伴认为,一个 namespace 下的 flushCache 只会清除当前 namespace 下的一级缓存与二级缓存,但这个想法是错误的。下面我们可以来测试一下效果。

为了测试方便,我们在 user.xml 中再定义一个 select ,将其 flushCache 声明为 true (当然使用任意 insert 、update 、delete 也都是可以的,它们的 flushCache 本身就是 true ):

    <select id="cleanCache" resultType="int" flushCache="true">
        select count(id) from tbl_user
    </select>

把这个 cleanCache 定义在 user.xml 中的目的,是为了确定清除 user 的二级缓存后,department 的二级缓存是否会被一起清除。

接下来,我们来测试一级缓存的效果:

public class SelectUseCacheApplication {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
    
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 连续查询两次同一个Department
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        Department department = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println(department);
        Department department2 = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("department == department2 : " + (department == department2));
        // 关闭第一个SqlSession使二级缓存保存
        sqlSession.close();
        
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
        // 再次查询Department
        Department department3 = departmentMapper2.findById("18ec781fbefd727923b0d35740b177ab");
        departmentMapper2.findAll();
    
        UserMapper userMapper = sqlSession2.getMapper(UserMapper.class);
        // 触发缓存清除
        userMapper.cleanCache();
        System.out.println("==================cleanCache====================");
        
        // 再再次查询Department
        Department department4 = departmentMapper2.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("department3 == department4 : " + (department3 == department4));
    
        sqlSession2.close();
    }
}

上面的代码可能稍微有点复杂,咱稍微解释一下。

  1. 首先我们先开一个 SqlSession ,查询 id 为 18ec781fbefd727923b0d35740b177abDepartment ,然后关闭 SqlSession 使一级缓存持久化到二级缓存; 2) 然后再开一个新的 SqlSession ,再次查询同样的 Department ,观察二级缓存是否生效; 3) 接着触发缓存清除,再查询一个相同的 Department ,观察二级缓存是否被清除。

接下来,我们运行 main 方法,观察控制台的日志输出:

[main] DEBUG source.pooled.PooledDataSource  - Created connection 1259652483. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4b14c583] 
[main] DEBUG pper.DepartmentMapper.findById  - ==>  Preparing: select * from tbl_department where id = ? 
[main] DEBUG pper.DepartmentMapper.findById  - ==> Parameters: 18ec781fbefd727923b0d35740b177ab(String) 
[main] DEBUG pper.DepartmentMapper.findById  - <==      Total: 1 
// --------------------第一次执行完departmentMapper.findById-------------------------
[main] DEBUG ybatis.mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.0 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4b14c583] 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@4b14c583] 
[main] DEBUG source.pooled.PooledDataSource  - Returned connection 1259652483 to pool. 
// --------------------第二次执行完departmentMapper.findById-------------------------
[main] DEBUG ybatis.mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.3333333333333333 
// --------------------关闭sqlSession1-------------------------
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG source.pooled.PooledDataSource  - Checked out connection 1259652483 from pool. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4b14c583] 
[main] DEBUG s.mapper.UserMapper.cleanCache  - ==>  Preparing: select count(id) from tbl_user 
[main] DEBUG s.mapper.UserMapper.cleanCache  - ==> Parameters:  
[main] DEBUG s.mapper.UserMapper.cleanCache  - <==      Total: 1 
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
==================cleanCache====================
[main] DEBUG ybatis.mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.5 
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4b14c583] 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@4b14c583] 
[main] DEBUG source.pooled.PooledDataSource  - Returned connection 1259652483 to pool.
  • 可以看出来,当第一次执行完 findById 后,一级缓存中已经存在数据了,所以第二次执行时没有打印 SQL 。之后 SqlSession 关闭,一级缓存持久化到二级缓存。

  • 再次开启一个新的 SqlSession 时,可以发现再次调用 findById 时依然没有 SQL 发送,说明二级缓存已经生效;然后调用 findAll 方法,让全部的 Department 查询加载到一级缓存;

  • 接下来执行 UserMappercleanCache ,清除掉二级缓存后,再次调用 findById 方法,日志中依然没有打印 SQL 发送,说明 UserMapper 清除的二级缓存不会影响到 DepartmentMapper ;但与此同时,findAll 方法重新打印了 SQL ,说明一级缓存被全部清除了!

所以,综合上面的观察和分析,也就得出结论了: **flushCache** 会清除全局一级缓存,以及本 namespace 下的二级缓存

1.1.3 fetchSize

这个配置本来不是 MyBatis 的,是 jdbc 的。要说这个,我们需要先了解一下 jdbc 的一些原生操作。

1.1.3.1 fetchSize本身存在于jdbc

public class JdbcFetchSizeApplication {
    
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis", "root", "123456");
        PreparedStatement ps = connection.prepareStatement("select * from tbl_department");
        // 在PreparedStatement上设置一次性抓取的结果行数
        ps.setFetchSize(2);
        ResultSet resultSet = ps.executeQuery();
        while (resultSet.next()) {
            System.out.println(resultSet.getString("name"));
        }
        resultSet.close();
        ps.close();
        connection.close();
    }
}

如上述代码中所呈现的,fetchSize 本来是 Statement 的一个属性配置,可以在执行 executeQuery 方法之前设置。

1.1.3.2 fetchSize的设计之初

在 jdbc 的 Statement 接口中,setFetchSize 方法的注释如下:

Gives the JDBC driver a hint as to the number of rows that should be fetched from the database when more rows are needed for ResultSet objects generated by this Statement. If the value specified is zero, then the hint is ignored. The default value is zero.

当此 Statement 生成的 ResultSet 对象需要更多行时,向 JDBC 驱动程序提供有关应从数据库中获取的行数的提示。如果指定的值为零,则忽略提示。默认值为零。

有点晦涩难懂,我们用稍微通俗易懂的方式来解释这个 fetchSize 的设计。

由于 MySQL 本身不支持 fetchSize 的设置,所以我们可以参考 Oracle 或者 PostgreSQL 。默认情况下,数据库驱动在发送 DQL 查询时,会一次性拉取整个查询结果到内存中(即 executeQuery ),当一次查询的数据量过大时,内存放不下那么多数据,就有可能造成 OOM 现象。这个时候, fetchSize 的作用就得以体现:数据库驱动查询到数据后,每次只从数据库拉取 **fetchSize** 指定量的数据,当这批数据都 next 完成后,再继续拉取下一批数据,以此来避免 OOM 现象的发生

还是有点难懂?举个例子吧!我现在的内存只够放 20 条数据,我发送一次 select * from tbl_department 的数据能查询到 50 条数据,如果一次性把这 50 条数据都放入内存,那指定是不够用的,OOM 在所难免;但如果我设置 fetchSize 为 10 ,每次只从数据库拉出来 10 条数据让我封装结果集,这 10 条封装完事了再继续拉取后 10 条,这样不就不会造成 OOM 的现象了嘛!

1.1.3.3 fetchSize的适用条件和场景

当然,fetchSize 也不是所有场景都能用,需要满足一些大前提:

  • 数据库环境支持( Oracle 可以,高版本的 PostgreSQL (7.4+) 也可以,但 MySQL 不行)

  • 执行 DQL 时,ConnectionautoCommit 必须为 false (即开启事务)

  • 查询结果的 ResultSet ,类型必须为 TYPE_FORWARD_ONLY (无法向相反的迭代方向滚动)(下面马上会提到)

  • 只有一次发送一条 DQL 时才有用,如果用分号隔开一次性发送多条 DQL ,也不好使( 如 select * from tbl_department; select * from tbl_user;

还有一点,fetchSize 使用的目的,是为了及时读取和处理数据,而不是把这些数据都读取并封装结果集。试想,如果是为了封装结果集的话,本来从数据库中读取出来的都够干爆你内存了,再封装一下结果集,岂不是双倍的“干爆”?所以这一点要明确哈。

1.1.4 resultSetType

resultSetType ,这个属性也不是 MyBatis 的,刚才在上面提到了 ResultSet 的类型,在 MyBatis 中就是利用这个属性来配置,不过我们一般根本不会用到它,只是作一下了解即可。

不过说到它,我们又得提一下 jdbc 的规范了。

1.1.4.1 默认情况下的结果集读取缺陷

根据 jdbc 的规范,Connection 对象在创建 Statement 时,可以指定 ResultSet 的类型参数,来控制查询动作执行的返回 ResultSet 类型。从 API 的角度来看,prepareStatement 方法有重载的可以传入 resultSetType 的方法:

可传入的值,在 ResultSet 接口中有定义:

    // 一般默认的类型,仅支持结果集向下迭代
    int TYPE_FORWARD_ONLY = 1003;
    // 可支持任何方向的滚动取得记录,对其他连接的修改不敏感
    int TYPE_SCROLL_INSENSITIVE = 1004;
    // 可支持任何方向的滚动取得记录,对其他连接的修改敏感
    int TYPE_SCROLL_SENSITIVE = 1005;

好,好,好,我们还是用比较容易理解的话来解释。

我们在执行 DQL ,获得 ResultSet 后,封装结果集也好,直接遍历结果集处理数据也好,我们都是这么写的:

    ResultSet resultSet = ps.executeQuery();
    // 遍历游标向下迭代
    while (resultSet.next()) {
        System.out.println(resultSet.getString("name"));
    }

每次获取一行新的数据,都是执行 **ResultSet** **next** 方法,从上往下迭代,迭代完成后,这个 ResultSet 的使命就结束了。如果有特殊的需求,需要再倒回去走一遍呢?

    // 遍历游标向下迭代
    while (resultSet.next()) {
        System.out.println(resultSet.getString("name"));
    }

    // 遍历游标向上迭代
    while (resultSet.previous()) {
        System.out.println("倒序 --- " + resultSet.getString("name"));
    }

对不起,不好使,默认情况下 ResultSet 只能从上往下走。怎么解决这个问题呢?就是改变 ResultSet 的类型,也就是上面提到的那 3 个常量。

1.1.4.2 jdbc中的resultSetType

刚上面说过了,在 prepareStatement 时,可以指定返回的 ResultSet 的类型,这里我们指定它的类型为 TYPE_SCROLL_INSENSITIVE ,也即允许滚动获取:

PreparedStatement ps = connection.prepareStatement("select * from tbl_department",
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);

之后正常执行上面的这段代码,可以发现两次 while 循环确实是一次正序读取,一次倒序读取:

全部部门
开发部
测试产品部
运维部
倒序 --- 运维部
倒序 --- 测试产品部
倒序 --- 开发部
倒序 --- 全部部门

TYPE_SCROLL_INSENSITIVE 相似的还有 TYPE_SCROLL_SENSITIVE ,它们的区别在于,如果在读取结果集时,数据库内的数据发生了改变,ResultSet 内的数据是否也跟着变。

1.1.4.3 MyBatis中配置resultSetType

在 MyBatis 中,默认情况下是不会主动设置 resultSetType 的,完全由数据库驱动决定;当然也可以指定,它指定的值有 3 种,分别与 jdbc 中的一一对应:

  • FORWARD_ONLYTYPE_FORWARD_ONLY

  • SCROLL_INSENSITIVETYPE_SCROLL_INSENSITIVE

  • SCROLL_SENSITIVETYPE_SCROLL_SENSITIVE

1.1.5 select标签的SQL编写

对于 SQL 的编写,想必不用小册多讲,各位都非常熟悉了吧!要么直接一气呵成,需要参数的地方用 #{} 设置好,遇到复杂的场景就使用动态 SQL 组合拼装。这一章我们不展开动态 SQL 的内容,下一章我们单独拿出来讲解。

1.2. insert update 和 delete - DML

由于 insert 、update 、delete 都属于 DML 语句,且它们的使用上大同小异,所以我们把这三种标签归为一个章节讨论。

DDL:(Data Definition Language 数据定义语言)用于操作对象及对象本身,这种对象包括数据库,表对象,及视图对象

DQL:(Data Query Language 数据查询语言 )用于查询数据

DML:(Data Manipulation Language 数据操控语言) 用于操作数据库对象对象中包含的数据

DCL:(Data Control Language 数据控制语句) 用于操作数据库对象的权限

1.2.1 标签的属性含义

我们还是先看看这些标签都有什么属性吧。这里面有不少属性是跟上面 select 类似或者完全相同的,这里我们只展示一些 select 没有的,或者有区别的属性:

属性描述备注

flushCache

执行 SQL 后会清空一级缓存(本地缓存)和二级缓存

默认值 true

useGeneratedKeys

开启使用,则 MyBatis 会使用 jdbc 底层的 getGeneratedKeys 方法,取出自增主键的值

仅适用于 insert 和 update ,默认值 false

keyProperty

配合 useGeneratedKeys 使用,需要指定传入参数对象的属性名,MyBatis 会使用 getGeneratedKeys 的返回值,或 insert 语句中的 selectKey 子元素,填充至指定属性中

仅适用于 insert 和 update ,无默认值

keyColumn

设置 useGeneratedKeys 生效的值对应到数据库表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是数据库表的第一列时,需要显式配置该属性

仅适用于 insert 和 update ,如果主键列不止一个,可以用逗号分隔多个属性名

除了 flushCache 之外,剩下 3 个都是在 insert 和 update 标签中才会用到的,下面咱聊聊它们几个。

1.2.2 useGeneratedKeys

这个说白了,就是在插入数据时,用数据库表的自增 id 作为主键。如果这个属性设置为 true ,则主键可以不用传,MyBatis 会在底层使用 jdbc 的 getGeneratedKeys 方法帮我们查出 id ,然后放入 id 属性中,并回填进实体类。这个属性可以跟 keyPropertykeyColumn 配合使用,下面我们可以来演示一下效果。

1.2.2.1 测试与keyProperty的配合

我们可以来搞一个简单的测试表,演示一下 useGeneratedKeys 配合 keyProperty 的作用效果:

create table tbl_dept2 (
    id int(11) not null auto_increment,
    name varchar(32) not null,
    tel varchar(18) default null,
    primary key (id)
);

然后是对应的 mapper 文件 test.xml

<mapper namespace="test">

    <insert id="save" useGeneratedKeys="true" keyProperty="id">
        insert into tbl_dept2 (name, tel) VALUES (#{name}, #{tel})
    </insert>
</mapper>

最后是测试运行类,我们直接调用

public class GeneratedKeysApplication {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        Department department = new Department();
        department.setName("hahaha");
        department.setTel("12345");
        sqlSession.insert("test.save", department);
        sqlSession.commit();
    
        System.out.println(department);
    }
}

执行 main 方法,观察控制台打印的 department 对象:

[main] DEBUG source.pooled.PooledDataSource  - Created connection 2129221032. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7ee955a8] 
[main] DEBUG                      test.save  - ==>  Preparing: insert into tbl_dept2 (name, tel) VALUES (?, ?) 
[main] DEBUG                      test.save  - ==> Parameters: hahaha(String), 12345(String) 
[main] DEBUG                      test.save  - <==    Updates: 1 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7ee955a8] 
Department{id='2', name='hahaha', tel='12345'}

可以发现,id 已经被成功的填充上了!

如果把 mapper.xml 中,insert 的这两个属性都去掉:

<insert id="save">
  insert into tbl_dept2 (name, tel) VALUES (#{name}, #{tel})
</insert>

那运行成功后的 department 打印是没有 id 属性值的:

Department{id='null', name='hahaha', tel='12345'}

由此就得出了一个结论:useGeneratedKeyskeyProperty 属性可以让我们在 insert 操作执行完成后,自动回填 id (当然前提是 id 自增)。

1.2.2.2 测试与keyColumn的配合

接下来我们再来测试一下 useGeneratedKeyskeyColumn 的相互配合。不过我们先来了解一个背景:

一般情况下,我们在设计数据库表结构时,都是拿第一列作为主键列,时间一长,大家都约定俗成了,看到第一列就知道是主键列。但真的保不齐会有一些外太空来的生物(滑稽)把数据库表的主键设置到别的列上,导致第一列不再是主键列了!这样虽然在 MySQL 中不会造成什么影响,但在 PostgreSQL 等数据库中会出现一个神奇的问题,下面我们来演示。

首先,我们先来准备一个 PostgreSQL 的数据库,并在 mybatis-config.xml 中配置好数据源:

<dataSource type="POOLED">
    <property name="driver" value="org.postgresql.Driver"/>
    <property name="url" value="jdbc:postgresql://localhost:5432/postgres"/>
    <property name="username" value="postgres"/>
    <property name="password" value="123456"/>
</dataSource>

然后在对应的数据库中创建一个跟上面一模一样的表:(注意 id 所在列不是第一列)

create table tbl_dept2 (
    name varchar(32) not null, 
    id serial primary key, 
    tel varchar(18)
);

此时 test.xml 中的配置还是只有 useGeneratedKeyskeyProperty

    <insert id="save" useGeneratedKeys="true" keyProperty="id">
        insert into tbl_dept2 (name, tel) VALUES (#{name}, #{tel})
    </insert>

接下来,我们直接运行上面的 GeneratedKeysApplication 类的 main 方法,观察控制台的 department 打印:

[main] DEBUG dbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [org.postgresql.jdbc.PgConnection@eb21112] 
[main] DEBUG           test.save  - ==>  Preparing: insert into tbl_dept2 (name, tel) VALUES (?, ?) 
[main] DEBUG           test.save  - ==> Parameters: hahaha(String), 12345(String) 
[main] DEBUG           test.save  - <==    Updates: 1 
[main] DEBUG dbc.JdbcTransaction  - Committing JDBC Connection [org.postgresql.jdbc.PgConnection@eb21112] 
Department{id='hahaha', name='hahaha', tel='12345'}

??????id 不是 int 类型吗?咋变成 "哈哈哈" 了?

可能有的小伙伴会怀疑,是不是 Department 模型类本身的 id 是 String 类型,所以导致出现的邪乎问题?那我们改一下嘛,把 id 的类型改为 Integer :

public class Department implements Serializable {
    private static final long serialVersionUID = -2062845216604443970L;
    
    private String name;
    
    private Integer id;
    
    private String tel;

然后再次运行 main 方法,这次更离谱,直接抛出异常了:

Caused by: org.postgresql.util.PSQLException: 不良的类型值 int : hahaha

好家伙,合着数据库驱动就是认准了非要把 hahaha 往 id 属性里塞了呗?那这肯定不合理呀!

可问题的原因是什么呢?这就是刚才在上面提到的,PostgreSQL 认为每张表的第一列都应该是主键列,所以就把第一列的值作为主键的返回值,放入模型类的 id 属性里了,由此引发了数据类型转换错误。

怎么解决呢?我们需要主动告诉他,哪个属性才是 id :

<insert id="save" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
  insert into tbl_dept2 (name, tel) VALUES (#{name}, #{tel})
</insert>

这样声明好之后,重新再运行 main 方法,这次控制台打印的就正常了:

Department{id=3, name='hahaha', tel='12345'}

注意 id 是 3,查一下数据库:

咦?数据库里咋只有两条数据呢?很简单,之前测试的时候不是报了一次异常嘛,那次异常导致事务回滚了,数据没添加上,但自增值已经被 +1 了,所以导致我们只看到了 1 和 3 ,没有看到 2 。

对于 insert 、update 、delete 标签,小册额外强调的主要就是这个 useGeneratedKeys 属性以及配合的这两个属性,其余的想必各位小伙伴都用的比较熟练了,小册也就不多啰嗦了。

1.3 resultMap - 结果集映射

<resultMap> 负责结果集映射,MyBatis 将其称为最重要、最强大的标签元素,可见重视程度之高。常规的使用方式小册也不多啰嗦,我们主要来研究几个相对特殊或者少见的使用方式。

1.3.1 使用pojo的构造方法

一般情况来讲,对于一个 pojo 来讲,它是不允许有任何显式定义的构造方法的,换句话说,它只能有本身的默认无参构造方法。当然,少数情况下,我们在配置 resultMap 的时候,还是会遇到一些特殊的场景,需要返回一些 VO 而非实体模型类,这个时候就需要 MyBatis 去调用它的有参构造方法了。MyBatis 对它的支持也是逐渐变好,在当下主流的 MyBatis 3.5 版本中,已经可以很好地处理有参构造方法的结果集映射了。下面我们来简单演示一下。

1.3.1.1 简单使用

我们直接拿现有的 Department 来做演示吧,我们分别显式的声明一个无参的构造方法,以及带 id 的有参构造方法:

public class Department implements Serializable {
    private String id;
    private String name;
    private String tel;
    
    public Department() {
    }
    
    public Department(String id) {
        this.id = id;
    }

这样既可以兼容之前的代码,又可以进行接下来的测试。

接下来,我们定义一个新的 resultMap ,并使用构造方法的 <constructor> 标签:

    <resultMap id="departmentWithConstructor" type="Department">
        <constructor>
            <idArg column="id" javaType="String"/>
        </constructor>
        <result property="name" column="name"/>
        <result property="tel" column="tel"/>
    </resultMap>

    <select id="findAll" resultMap="departmentWithConstructor">
        select * from tbl_department
    </select>

然后就可以编码测试了,编码的内容我们都写了非常多遍了:

public class ResultMapApplication {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        List<Department> departmentList = departmentMapper.findAll();
        departmentList.forEach(System.out::println);
    }
}

运行 main 方法,控制台可以正常打印出所有的部门信息,证明用 <constructor> 标签也是完全没有问题的。

1.3.1.2 使用中要注意的

注意一个使用中的细节,<constructor> 标签的子标签中,都有一个 name 属性:

这个 name 是对应的实体类的成员属性名,一般情况下我们不需要写。不过,如果真的需要声明 name 属性的话,需要在实体类中配合着写一个注解:@Param

    public Department(@Param("idd") String id) {
        this.id = id;
    }

@Paramvalue 可以随便写,比方说上面小册特意多写了一个 d ,相配合的,需要在上面的 resultMap 中显式声明 name 属性:

<resultMap id="departmentWithConstructor" type="com.linkedbear.mybatis.entity.Department">
    <constructor>
        <!-- 注意name要与上面的@Param注解的value一致 -->
        <idArg column="id" javaType="String" name="idd"/>
    </constructor>
    <result property="name" column="name"/>
    <result property="tel" column="tel"/>
</resultMap>

这样我们再执行 ResultMapApplicationmain 方法时,依然能够正常执行。小伙伴也可以自行测试一下,如果两边的 name 没对上,main 方法会在一开始启动时就报 BuilderException 的(提示 Error in result map )。

1.3.2 引用其他resultMap

还记得之前在基础回顾中我们写的那个最基本的 userMap 吗:

<resultMap id="userMap" type="com.linkedbear.mybatis.entity.User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="age" column="age"/>
    <result property="birthday" column="birthday"/>
    <association property="department" javaType="com.linkedbear.mybatis.entity.Department">
        <id property="id" column="department_id"/>
        <result property="name" column="department_name"/>
    </association>
</resultMap>

幸好 Department 实体类的属性不算多,要是属性超级多呢。。。来上它十来个二十几个,那我们配起来得多费劲啊。。。所以才有了一些取代的解决方案,要么是用延迟加载,或者,我们可以通过引用外部 resultMap 来实现。

1.3.2.1 现成的resultMap+prefix

比方说,我们之前在 department.xml 中不是已经定义过最基本的 department 映射关系了嘛:

<resultMap id="department" type="com.linkedbear.mybatis.entity.Department">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="tel" column="tel"/>
</resultMap>

那么,我们就可以直接拿来用了。找到 user.xml 中的 findAll ,我们改一下 resultMap :

<resultMap id="userWithPrefix" type="com.linkedbear.mybatis.entity.User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="age" column="age"/>
    <result property="birthday" column="birthday"/>
    <association property="department" javaType="com.linkedbear.mybatis.entity.Department"
                 resultMap="com.linkedbear.mybatis.mapper.DepartmentMapper.department" columnPrefix="department_"/>
</resultMap>

<select id="findAll" resultMap="userWithPrefix"> <!-- 注意这里的resultMap是上面新定义的 -->
    select usr.*, dep.name as department_name, dep.tel as department_tel
    from tbl_user usr
    left join tbl_department dep on usr.department_id = dep.id
</select>

注意看上面的 userWithPrefix 定义,<association> 标签中直接引用了上面的那个 department 的 resultMap ,只不过多了一个 **columnPrefix="department_"** 的配置。它的用途想必小册不用多解释,小伙伴也能猜得到,它可以在封装结果集时,自动取出 "指定前缀 + column" 的列封装到指定的 resultMap 中。

感觉好绕啊,咱就以上面的查询为例吧,findAll 的 SQL 语句放到数据库中执行的结果应该如下:

这里面有关 department 部门信息的列,刚好跟 Department 实体类能一一对应,也跟 deparment 那个 resultMap 能对应上,只不过这几列都有一个共同的前缀是 department_ ,所以我们就可以把这几列也拿出来,告诉 MyBatis ,这几列需要给我拼个 department_ 的前缀,封装到 Department 实体类对象中,MyBatis 就可以帮我们这样干。

用这种方式替换了之后,下面我们测试一下效果:

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.findAll();
userList.forEach(System.out::println);

运行 main 方法,观察控制台打印的 User 对象:

User{id='09ec5fcea620c168936deee53a9cdcfb', name='阿熊', department=Department{id='18ec781fbefd727923b0d35740b177ab', name='开发部', tel='123'}}
User{id='0e7e237ccac84518914244d1ad47e756', name='hahahaha', department=Department{id='18ec781fbefd727923b0d35740b177ab', name='开发部', tel='123'}}
User{id='5d0eebc4f370f3bd959a4f7bc2456d89', name='老狗', department=Department{id='ee0e342201004c1721e69a99ac0dc0df', name='运维部', tel='456'}}

可以发现,Department 的所有属性均已填充,说明这种引用其他 resultMap 的方式是完全可以的。

1.3.2.2 直接引用resultMap

当然,如果查出来的列中,对应到 Department 实体类的属性不完全都带前缀的话(例如 department_id 、department_name 、tel ),那 resultMap + prefix 的办法就不奏效了,这种情况我们只能再定义一个新的 resultMap ,然后直接引用,就像这样:

<resultMap id="userWithPrefix" type="com.linkedbear.mybatis.entity.User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="age" column="age"/>
    <result property="birthday" column="birthday"/>
    <association property="department" javaType="com.linkedbear.mybatis.entity.Department"
                 resultMap="com.linkedbear.mybatis.mapper.DepartmentMapper.departmentWithPrefix"/>
</resultMap>

<resultMap id="departmentWithPrefix" type="com.linkedbear.mybatis.entity.Department">
    <id property="id" column="department_id"/>
    <result property="name" column="department_name"/>
    <result property="tel" column="tel"/>
</resultMap>

用这种方式同样可以,小伙伴们可以自行测试验证。

1.3.3 resultMap的继承

跟 SpringFramework 中的 BeanDefinition 继承类似,resultMap 也有继承的概念。引入继承,使 resultMap 具备了层次性和通用性。我们可以通过一个例子来体会一下 resultMap 继承的特性有什么好处。

回顾一下我们之前写一对多时,我们需要在 department 的 resultMap 中添加一个 collection ,来关联加载所有的 User ,但是实际情况下有些场景根本不需要这些 User ,那加载出来就是浪费性能的,徒增功耗!以前我们的解决方法是这样的:

<resultMap id="department" type="com.linkedbear.mybatis.entity.Department">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="tel" column="tel"/>
</resultMap>

<resultMap id="departmentWithUsers" type="com.linkedbear.mybatis.entity.Department">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="tel" column="tel"/>
    <collection property="users" ofType="com.linkedbear.mybatis.entity.User"
                select="com.linkedbear.mybatis.mapper.UserMapper.findAllByDepartmentId" column="id"/>
</resultMap>

这样写倒是没错,但 tbl_department 表中的普通字段映射却重复出现了,维护成本会增加。MyBatis 自然帮我们想到了这一点,所以我们可以这样优化一下:

<resultMap id="department" type="com.linkedbear.mybatis.entity.Department">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="tel" column="tel"/>
</resultMap>

<resultMap id="departmentWithUsers" type="Department" extends="department">
    <collection property="users" ofType="com.linkedbear.mybatis.entity.User"
                select="com.linkedbear.mybatis.mapper.UserMapper.findAllByDepartmentId" column="id"/>
</resultMap>

<select id="findAll" resultMap="departmentWithUsers">
    select * from tbl_department
</select>

看,department 这个 resultMap 中只配置单表字段的映射,关联集合的查询用 departmentWithUsers 这个 resultMap ,让它继承 department ,也可以同时拥有单表的字段映射关系。

如此配置好之后,我们可以改一下 findAll 的 resultMap ,并实际测试一下。当然结果肯定是可行的,只是小册就不贴测试结果了(主要是太长了),小伙伴们可以动手写一写,试一下。

1.3.4 鉴别器

最后咱再介绍一种比较特殊的结果集映射,叫 discriminator 鉴别器映射。鉴别器,无非就是根据某些条件,决定如何做 / 如何选。下面我们通过一个简单的需求来讲解 discriminator 的使用。

1.3.4.1 需求

现有的 tbl_user 表中有一个 deleted 属性,代表是否逻辑删除。我们的需求是,当 deleted 属性值为 0 时,代表未删除,查询用户信息时需要把部门信息一并带出;deleted 为 1 时,代表用户已删除,不再连带查询部门信息。

为了区分两种不同的用户,我们先把 tbl_user 中的 hahahaha 用户,deleted 属性改为 1 :

update tbl_user set deleted = 1 where id = '0e7e237ccac84518914244d1ad47e756';

1.3.4.2 使用鉴别器

根据需求来说,是需要先查出结果,后决定如何封装结果集,所以我们可以先全部查出,后决定如何封装,也可以先查出 tbl_user 的主表,再根据 deleted 属性延迟加载部门信息。小册此处选择第二种方案了哈,小伙伴在实际练习时可以都写一下。

首先我们先来声明一个全新的 statement :

<select id="findAllUseDiscriminator" resultMap="userWithDiscriminator">
    select * from tbl_user
</select>

针对这个新声明的 resultMap ,我们也定义出来:

<resultMap id="userWithDiscriminator" type="com.linkedbear.mybatis.entity.User">
    <discriminator column="deleted" javaType="boolean">
        <case value="false" resultMap="userlazy"/>
        <case value="true" resultType="com.linkedbear.mybatis.entity.User"/>
    </discriminator>
</resultMap>

我们来看一下这种写法,这里面只有一个 <discriminator> 的子标签,它可以取出查询结果数据集中的某一列,转换为指定的类型,并进行类似于 switch-case 的比较,根据比较的相同的值,使用对应的 resultMap 或者 resultType 。

tbl_user 表中的 deleted 属性,对应的数据类型是 tinyint ,它就相当于 Java 中的 boolean ,0 代表 false ,1 代表 true 。那我们就可以声明,当 deleted 为 false 时,用延迟加载的那个 userlazy 就可以,这样查 User 的时候能顺便把 Department 也查出来;deleted 为 true 时,只查本表的属性,那就直接用 resultType 指定 User 类型就够了。

1.3.4.3 测试运行

一切准备就绪,下面我们简单编写一下测试代码:

List<User> userList2 = sqlSession.selectList("com.linkedbear.mybatis.mapper.UserMapper.findAllUseDiscriminator");
userList2.forEach(System.out::println);

然后运行 main 方法,观察控制台的 User 打印:

User{id='09ec5fcea620c168936deee53a9cdcfb', name='阿熊', department=Department{id='18ec781fbefd727923b0d35740b177ab', name='开发部', tel='123'}}
User{id='0e7e237ccac84518914244d1ad47e756', name='hahahaha', department=null}
User{id='5d0eebc4f370f3bd959a4f7bc2456d89', name='老狗', department=Department{id='ee0e342201004c1721e69a99ac0dc0df', name='运维部', tel='456'}}

可以发现 hahahaha 的 department 属性没有了,也就说明 discriminator 的作用生效了。

1.4 cache - 缓存

最后我们来简单提一嘴 mapper.xml 中的缓存,默认情况下 MyBatis 只会开启基于 SqlSession 的一级缓存,二级缓存默认不会开启。二级缓存,也可以理解为基于 SqlSessionFactory 级别的缓存 / namespace 范围的缓存,一个 namespace 对应一块二级缓存。如果需要为特定的 namespace 开启二级缓存,则可以在对应的 mapper.xml 中声明一个 <cache> 标签:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.linkedbear.mybatis.mapper.DepartmentMapper">
    <cache />
    
    <!-- ... statement ... -->
</mapper>

因为 MyBatis 默认是全局开启二级缓存的,所以不需要额外的配置。

这里有小伙伴反映理解可能有歧义,小册补充一个比喻:

MyBatis 默认全局开启二级缓存,就好比一个通了电的插排;每个 mapper.xml 中声明 <cache /> 标签后,就好比往插排上插了一样电器。当插排通电时,所有插着的电器都能正常使用,但如果整个插排的总开关断开(禁用二级缓存),那所有的电器都就不能用了。

另外还有一个 <cache-ref> 标签,它是引用其他 namespace 的二级缓存的,引入之后本 mapper.xml 中的操作,也会影响 <cache-ref> 引用的缓存。

2. mapper.xml解析机制

上篇有提到,最后一个节点的解析是 mapper ,也就是解析 MyBatis 全局配置文件中,引入的 mapper.xml 的那些路径。而这里面的解析,都是使用一个 **XMLMapperBuilder** 的 API 完成的。:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 包扫描Mapper接口
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                // 处理resource加载的mapper.xml
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    // 处理url加载的mapper.xml
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    // 注册单个Mapper接口
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

那么我们接下来研究的重点,就是深入 XMLMapperBuilder 中,看看它如何解析 mapper.xml 的。

Debug 的载体,我们依然选用一开始复制进来的 MyBatisApplication6 ,Debug 运行后进入 mapperElement 方法,断点停住。

2.1 XMLMapperBuilder

先大体看一下 XMLMapperBuilder 本身吧,它的内部构造还是比较值得研究的。

2.1.1 继承关系和内部成员

翻开源码,从继承关系上赫然发现 XMLMapperBuilder ,也是继承自 BaseBuilder 的:

public class XMLMapperBuilder extends BaseBuilder {

  private final XPathParser parser;
  private final MapperBuilderAssistant builderAssistant;
  private final Map<String, XNode> sqlFragments;
  private final String resource;
  ...
}

是不是再一次体会到之前第 7 章说的,**BaseBuilder** 是一个基础的构造器啊。

然后关注一下成员:

  • XPathParser parser :它是解析 xml 文件的解析器,此处也用来解析 mapper.xml。

  • MapperBuilderAssistant builderAssistant :构造 Mapper 的建造器助手(至于为什么是助手,简单地说,它的内部使用了一些 Builder ,帮我们构造 ResultMapMappedStatement 等,不需要我们自己操纵,所以被称之为 “助手” )

  • Map<String, XNode> sqlFragments :封装了可重用的 SQL 片段(就是上一章提到的 <sql> 片段)

  • String resource :mapper.xml 的文件路径

一眼看下来,也没什么特别好强调的,下面碰到什么再专门拿出来说吧。

2.1.2 构造方法定义

上面的源码中都会先调用 XMLMapperBuilder 的好几个参数的构造方法,而构造方法向下走之后,都是一组赋值操作,也没什么意思。

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
            configuration, resource, sqlFragments);
}

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
}

只需要注意一个小细节即可:MapperBuilderAssistant 在此处创建。这个 MapperBuilderAssistant 具体都干了什么,后面我们马上就可以看到了。

2.1.3 核心parse方法

构造完成后就可以调用 parse 方法了(此时 mapper.xml 已经被 IO 读取封装为 InputStream ),而这个方法的信息量有点大,我们一行一行解析。先留个大体的注释在源码中:

public void parse() {
    // 如果当前xml资源还没有被加载过
    if (!configuration.isResourceLoaded(resource)) {
        // 2. 解析mapper元素
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        // 3. 解析和绑定命名空间
        bindMapperForNamespace();
    }

    // 4. 解析resultMap
    parsePendingResultMaps();
    // 5. 解析cache-ref
    parsePendingCacheRefs();
    // 6. 解析声明的statement
    parsePendingStatements();
}

下面我们就源码中标注了序号的关键代码,逐行解析。不过具体特别深入的我们不做探究,后面小册有专门解析生命周期和执行流程的章节,到那时候我们再展开仔细研究 MyBatis 内部的细节。

2.2 configurationElement

configurationElement(parser.evalNode("/mapper")); 这句代码只从最后的参数,就知道是解析 mapper.xml 的最顶层 <mapper> 标签了。这部分的解析,会把所有的标签都扫一遍,具体我们可以先看一眼源码和注释:

private void configurationElement(XNode context) {
    try {
        // 提取mapper.xml对应的命名空间
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        // 解析cache、cache-ref
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        // 解析提取parameterMap(官方文档称已废弃,不看了)
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        // 解析提取resultMap
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 解析封装SQL片段
        sqlElement(context.evalNodes("/mapper/sql"));
        // 构造Statement
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}

然后我们逐行解释这些标签的解析,底层都干了什么。

2.2.1 提取命名空间

// 提取mapper.xml对应的命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
  throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);

初学 MyBatis 的时候,我们就知道,每个 mapper.xml 都需要声明 namespace ,哪怕是我们瞎写那种 abcdefg 的都行,但不能没有,源码中这里就体现了非空检查。命名空间非空的设计,一方面是考虑到二级缓存(一个 namespace 对应一个二级缓存),另一方面也是考虑到可能不同的 mapper.xml 中存在同名的 statement (比方说 department 和 user 都有 findAll ,这个时候通过 namespace 就可以很好地区分开这两个 statement 了)。

2.2.2 解析cache、cache-ref

cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));

这两步的核心动作,是解析看一下 mapper.xml 中有没有引用其他 namespace 的二级缓存,以及看一下本 namespace 下有没有开启二级缓存,如果有的话,自己配置一下。

二级缓存的解析主要是 <cache> 标签,这里面能定义的属性挺多的,底层会根据这些配置,借助 MapperBuilderAssistant 构建出 Cache 缓存对象的实现,而 MapperBuilderAssistant 又是利用 CacheBuilder 构造的 Cache 对象,这里面有建造者和装饰者的体现,我们到后面设计模式的部分还会讲到。

private void cacheElement(XNode context) {
    if (context != null) {
        // 默认的类型是PERPETUAL,也即PerpetualCache
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        // 默认的过期策略 LRU
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        // 获取其他属性
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        // 2.2.3 创建Cache对象
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

public Cache useNewCache(Class<? extends Cache> typeClass, 
        Class<? extends Cache> evictionClass,
        Long flushInterval, Integer size, boolean readWrite,
        boolean blocking, Properties props) {
    // 建造器!
    Cache cache = new CacheBuilder(currentNamespace)
                          .implementation(valueOrDefault(typeClass, PerpetualCache.class))
                          .addDecorator(valueOrDefault(evictionClass, LruCache.class))
                          .clearInterval(flushInterval)
                          .size(size)
                          .readWrite(readWrite)
                          .blocking(blocking)
                          .properties(props)
                          .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

2.2.3 解析提取resultMap

// 解析提取resultMap
resultMapElements(context.evalNodes("/mapper/resultMap"));

resultMap 结果集映射配置是 MyBatis 最强大的特性之一,自然它的处理逻辑会相当复杂

解析 <resultMap> 结果集映射大概可以分为三个步骤:

  • 解析结果集的目标类型。

  • 解析结果集的映射配置。

  • 封装构建 ResultMap 对象。

其中第二个步骤是最复杂的,根据不同的映射规则和标签,有不同的处理方式。

2.2.3.1 ResultMapping的结构

先回顾一个概念:resultMap 是一个完整的结果集映射配置,resultMapping 是一个结果集映射配置中的某一个项(组成部分),它们是组合关系。说到这里,可能小伙伴还没有意识到 resultMapping 到底是个啥,它的结构有什么,它的地位是什么。这样吧,小册把 resultMapping 对应封装的模型,它里面的内部成员贴出来,各位看一看:

public class ResultMapping {

    private Configuration configuration;
    // 实体类的属性名
    private String property;
    // 结果集的列名
    private String column;
    // 映射属性的类型(可能是关联属性)
    private Class<?> javaType;
    private JdbcType jdbcType;
    private TypeHandler<?> typeHandler;
    // 直接引用另一个resultMap的全限定名
    private String nestedResultMapId;
    // 关联查询的statement全限定名(用于延迟加载)
    private String nestedQueryId;
    private Set<String> notNullColumns;
    // 引用其它resultMap时列名的前缀
    private String columnPrefix;
    private List<ResultFlag> flags;
    private List<ResultMapping> composites;
    private String resultSet;
    private String foreignColumn;
    private boolean lazy;

抓住源码中带注释的几个属性,我们就可以意识到,ResultMapping 已经把一条映射结果集的某一个属性的映射定义的明明白白了。一组 ResultMapping 可以构成一个 ResultMap ,而一个 ResultMap 就对应 mapper.xml 中的一个 <resultMap> 标签。


进入代码:

private void resultMapElements(List<XNode> list) {
    for (XNode resultMapNode : list) {
        try {
            resultMapElement(resultMapNode);
        } catch (IncompleteElementException e) {
            // ignore, it will be retried
        }
    }
}

一个 mapper.xml 文件可能不止一个 <resultMap> 标签,这里肯定有一个 for 循环啦。

注意这里的 try-catch 结构是放在 for 循环体里的,这么做是为了防止某一个 resultMap 解析失败时,导致连带着 mapper.xml 中其他的 resultMap 也没法解析。这样设计后,即便某一个 resultMap 解析挂掉了,也可以继续解析剩余的 resultMap 。

进入单个 resultMap 的解析方法,这里面的逻辑看上去挺多,但实际上条理还是很清晰的,我们可以先通读一遍源码,配合着我标注的注释理解一下:

private ResultMap resultMapElement(XNode resultMapNode) {
    return resultMapElement(resultMapNode, Collections.emptyList(), null);
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 解析resultMap映射的目标结果集实体类型
    String type = resultMapNode.getStringAttribute("type",
                         resultMapNode.getStringAttribute("ofType", 
                         resultMapNode.getStringAttribute("resultType", 
                         resultMapNode.getStringAttribute("javaType"))));
    // 加载目标结果集实体类型
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
        typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    
    List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    // 解析resultMap的子标签,并封装为resultMapping
    for (XNode resultChild : resultChildren) {
        if ("constructor".equals(resultChild.getName())) {
            processConstructorElement(resultChild, typeClass, resultMappings);
        } else if ("discriminator".equals(resultChild.getName())) {
            discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        } else {
            List<ResultFlag> flags = new ArrayList<>();
            if ("id".equals(resultChild.getName())) {
                flags.add(ResultFlag.ID);
            }
            resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
        }
    }
    
    // 获取resultMap的id、继承的resultMap id、autoMapping
    String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    // 利用ResultMapResolver处理resultMap
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, 
            typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
        return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
        // 解析失败,说明resultMap标签的信息不完整,记录在全局Configuration中,并抛出异常
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

走完一遍源码,是不是感觉没有很混乱?思路还是蛮清晰的?其实这也是体现了 MyBatis 的源码中清晰的逻辑思路。下面我们分段来解释这段源码中比较复杂的部分。

2.2.3.2 解析结果集目标类型

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    String type = resultMapNode.getStringAttribute("type",
                         resultMapNode.getStringAttribute("ofType", 
                         resultMapNode.getStringAttribute("resultType", 
                         resultMapNode.getStringAttribute("javaType"))));
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
        typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    // ......
}

这一段的目的就是解析目标结果集的实体类类型,上面提到的 4 个属性都可以写,而且必定得有一个写,如果都不写,在解析 xml 时就会报 DTD 异常(需要属性 "type" , 并且必须为元素类型 "resultMap" 指定该属性)。优先级依次是 type > ofType > resultType > javaType

2.2.3.3 解析结果集映射配置

// ......
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
// 解析resultMap的子标签,并封装为resultMapping
for (XNode resultChild : resultChildren) {
  if ("constructor".equals(resultChild.getName())) {
    processConstructorElement(resultChild, typeClass, resultMappings);
  } else if ("discriminator".equals(resultChild.getName())) {
    discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
  } else {
    List<ResultFlag> flags = new ArrayList<>();
    if ("id".equals(resultChild.getName())) {
      flags.add(ResultFlag.ID);
    }
    resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
  }
}
// ......

这一段又很复杂,这里主要干的活是解析 <resultMap> 的子标签们,由于只有 3 种标签可以写(普通映射、构造器映射、鉴别器映射),所以这里的 if-else 结构也看上去比较简单,当然也仅限于看上去。内部解析这几个子标签的内容又比较复杂了,我们先从最简单的 else 中看起。

2.2.3.3.1 解析普通映射标签

普通映射标签有 id 、result 、association 、collection 四个标签,也都是我们在学习 MyBatis 基础的时候就用到的标签了。解析的核心方法是 buildResultMappingFromContext ,我们进去看一下:

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
        property = context.getStringAttribute("name");
    } else {
        property = context.getStringAttribute("property");
    }
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    String nestedResultMap = context.getStringAttribute("resultMap", () ->
            processNestedResultMappings(context, Collections.emptyList(), resultType));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", 
                             configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    // 结果集类型、typeHandler类型的解析
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return builderAssistant.buildResultMapping(resultType, property, column, 
                   javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, 
                   notNullColumn, columnPrefix, typeHandlerClass, 
                   flags, resultSet, foreignColumn, lazy);
}

好家伙,合着这一段都是取属性呗,那就没意思了呀(就是有点辣眼)。。。

这个方法我们要知道的,就是将一行 **<result property="" column="" />** 标签解析封装为一个 **ResultMapping** 即可。

思考一个问题:这个方法可以处理 4 个标签 <id><result><association><collection>,但是这里面几乎没有标签的区分(仅有 <id> 判断过一次),那 MyBatis 怎么就能保证一个方法通吃呢?

答案在这个方法的最后一行,因为我们把解析的标签中所有的属性都取出来,一股脑的扔到 MapperBuilderAssistant 中了,那自然 MapperBuilderAssistant 会帮我们处理吧。我们进到 buildResultMapping 方法中一探究竟:

public ResultMapping buildResultMapping(Class<?> resultType, String property,
        String column, Class<?> javaType, JdbcType jdbcType, String nestedSelect,
        String nestedResultMap, String notNullColumn, String columnPrefix,
        Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags,
        String resultSet, String foreignColumn, boolean lazy) {
    // 看看这个映射结果集的类型,有没有TypeHandler能处理它
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    // 处理复杂结果集的列名(极特殊用法)
    List<ResultMapping> composites;
    if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
        composites = Collections.emptyList();
    } else {
        composites = parseCompositeColumnName(column);
    }
    // 交给建造器处理
    return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
            .jdbcType(jdbcType)
            .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
            .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
            .resultSet(resultSet)
            .typeHandler(typeHandlerInstance)
            .flags(flags == null ? new ArrayList<>() : flags)
            .composites(composites)
            .notNullColumns(parseMultipleColumnNames(notNullColumn))
            .columnPrefix(columnPrefix)
            .foreignColumn(foreignColumn)
            .lazy(lazy)
            .build();
}

上面的逻辑我们可以忽略掉,重要的是最后,这怎么是一个 ResultMapping 的建造者一串到底了?说好的四种不同的映射标签处理呢?

好,这个时候我们就要静下心来好好想想,为什么 MyBatis 没有区分开来。想一下,四种标签分别都需要定义哪些属性呢?

  • <id>columnproperty

  • <result>columnproperty

  • <association>propertyjavaType

  • <collection>columnpropertyofType

可以发现,四种标签所需要定义的属性,只有 <id><result> 是一样的,其余的都不一样!所以 MyBatis 只区分了 <id><result> ,其余的一概通吃,因为只要添加了 <id> 的标识后,四种标签仅靠定义的属性就可以区分开来了!

至于最后的 ResultMapping 具体构建,那就是 ResultMapping.Builder 建造者的逻辑了,内容相对简单,其实就是一个一个属性的设置罢了。

2.2.3.3.2 处理constructor:解析构造器标签

前面第 8 章我们已经接触了 <constructor> 标签的使用,也知道它的内部其实还是封装类似于 <id><result> 等标签,所以它的处理逻辑基本上是大同小异:

private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
    List<XNode> argChildren = resultChild.getChildren();
    for (XNode argChild : argChildren) {
        List<ResultFlag> flags = new ArrayList<>();
        flags.add(ResultFlag.CONSTRUCTOR);
        if ("idArg".equals(argChild.getName())) {
            flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
    }
}

得了,这不跟上面一个样了吗?最终还是调用的 buildResultMappingFromContext 方法,把那一行一行的结果集映射都封装为 ResultMapping 完事。

不过这里要额外注意一个细节,上面的每一行结果集映射中,都会对应一个 List<ResultFlag> flags 的家伙,而且在解析 <constructor> 标签的时候,它会先给 flags 集合中添加一个 ResultFlag.CONSTRUCTOR 的元素,这个元素会在 buildResultMappingFromContext 方法中起作用:

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
        // <constructor>标签用name
        property = context.getStringAttribute("name");
    } else {
        // 普通的标签用property
        property = context.getStringAttribute("property");
    }
}

可以发现,<constructor> 标签中取属性名要用 name 而不是 property ,这个小细节我们可以在 mapper.xml 中发现:

或许我们没有感知到,一是因为一般情况下结果集映射的实体类都只有缺省的无参构造器,用不到 <constructor> 属性;二是写的时候也没有特别的去找,看到 name 或许也会理所当然的觉得它就是(常码代码的各位都有一种所谓的“感觉”,不怎么过大脑思考就顺手写出来了,有同感的记得评论区扣**【俺也一样】**)。

2.2.3.3.3 处理discriminator:解析鉴定器标签

先简单回顾下鉴定器的使用,它可以根据查询查询结果集的某一列数据,动态选择封装结果集的 ResultMap 。

下面是一个简单的使用,它会根据是否用户是否被逻辑删除,决定要不要延迟加载所在部门的信息。

    <resultMap id="userWithDiscriminator" type="com.linkedbear.mybatis.entity.User">
        <discriminator column="deleted" javaType="boolean">
            <case value="false" resultMap="userlazy"/>
            <case value="true" resultType="com.linkedbear.mybatis.entity.User"/>
        </discriminator>
    </resultMap>

OK 简单回顾了使用,下面我们看源码:

private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String typeHandler = context.getStringAttribute("typeHandler");
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    // 解析<discriminator>的<case>子标签,并封装到Map中
    Map<String, String> discriminatorMap = new HashMap<>();
    for (XNode caseChild : context.getChildren()) {
        String value = caseChild.getStringAttribute("value");
        String resultMap = caseChild.getStringAttribute("resultMap", 
                processNestedResultMappings(caseChild, resultMappings, resultType));
        discriminatorMap.put(value, resultMap);
    }
    // 注意构造的是Discriminator而不是ResultMapping
    return builderAssistant.buildDiscriminator(resultType, column, 
             javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}

源码中主要干的事情,是获取到判断依据的列,以及处理的 case 分支。这里我们重点关注一下这个 discriminatorMap 封装的结果:

可以发现,它只是把我们定义的两种情况,以及对应的 resultType / resultMap 存起来了而已。注意这个 true 对应的值,它并没有存放实际的 resultType ,而是一大串我们看不懂的东西,其实这一大串东西,对应的就是 resultType="com.linkedbear.mybatis.entity.User" ,它这么起名只是为了标明来源。

至于这一大串是怎么来了,小册就不展开讲解了,小伙伴们可以关注一下 resultMapElement 这个方法,由于一切 resultType 最终也会被 MyBatis 封装为 ResultMap ,所以这里可以找到对应的逻辑。

这个 mapper_resultMap[userWithDiscriminator]_discriminator_case[false] 本质是对应的 ResultMap 的 id 。

解析出 Map 后,下一步就是构建了,它依然使用 MapperBuilderAssistant 帮忙构建,我们进入 buildDiscriminator 方法:

public Discriminator buildDiscriminator(Class<?> resultType, String column,
        Class<?> javaType, JdbcType jdbcType, Class<? extends TypeHandler<?>> typeHandler,
        Map<String, String> discriminatorMap) {
    ResultMapping resultMapping = buildResultMapping(
            resultType, null, column, javaType, jdbcType, null, null, null, 
            null, typeHandler, new ArrayList<>(), null, null, false);
    Map<String, String> namespaceDiscriminatorMap = new HashMap<>();
    for (Map.Entry<String, String> e : discriminatorMap.entrySet()) {
        String resultMap = e.getValue();
        // 注意这里如果应用自身的其他resultMap,会在前面追加当前mapper.xml对应的namespace
        resultMap = applyCurrentNamespace(resultMap, true);
        namespaceDiscriminatorMap.put(e.getKey(), resultMap);
    }
    return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build();
}

可以发现这里面其实没有什么神秘的,核心还是借助 Discriminator 的建造器帮忙创建出 Discriminator 对象来。

同样的,这个鉴定器我们平时几乎不用,所以小伙伴们看一下,大概有个认识就 OK 了。

鉴别器的解析是最特别的一个了,它最终构建的类型都不一样了,咱先扫一下源码:

private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String typeHandler = context.getStringAttribute("typeHandler");
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    // 解析<discriminator>的<case>子标签,并封装到Map中
    Map<String, String> discriminatorMap = new HashMap<>();
    for (XNode caseChild : context.getChildren()) {
        String value = caseChild.getStringAttribute("value");
        String resultMap = caseChild.getStringAttribute("resultMap", 
                processNestedResultMappings(caseChild, resultMappings, resultType));
        discriminatorMap.put(value, resultMap);
    }
    // 注意构造的是Discriminator而不是ResultMapping
    return builderAssistant.buildDiscriminator(resultType, column, 
             javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}

2.2.3.4 封装构建ResultMap

<resultMap> 标签解析的最后一部分,它会用一个 ResultMapResolver 来处理,并最终构造出 ResultMap 对象。

// ......
    // 获取resultMap的id、继承的resultMap id、autoMapping
    String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    // 利用ResultMapResolver处理resultMap
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, 
            typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
        return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
        // 解析失败,说明resultMap标签的信息不完整,记录在全局Configuration中,并抛出异常
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

这个 ResultMapResolver ,说起来有点搞笑,它的 resolve 方法,就是调了 MapperBuilderAssistant 的方法:

public ResultMap resolve() {
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}

那问题就来了,它为什么要搞这么一出呢?闲得慌吗?哎,人家这么设计肯定是有原因的,这里是一个伏笔,下面第 4 小节会有呼应。

至于 MapperBuilderAssistant 底层都干了什么,现在深入进去难免各位会头晕,所以咱都是跟上面一样,统一放在后面的生命周期章节讲解,这部分各位只需要知道:最终干活的都是 **MapperBuilderAssistant** 就得了。

2.2.4 提取SQL片段

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 提取SQL片段
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

接下来是提取各个 mapper.xml 中的 SQL 片段了,这里面的大规则我们都很清楚了:如果有显式声明 databaseId ,那只有符合当前全局 databaseId 的 SQL 片段会提取;如果没有声明 databaseId ,则会全部提取

所以下面的源码中,我们会发现,它解析了两遍 SQL 片段,而且在每一次循环解析中,都会判断一次 SQL 片段是否匹配当前 databaseId ,匹配的话就会放到一个 sqlFragmentsMap 中:(关键代码已标有注释)

private void sqlElement(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        // 先全部过一遍,提取出匹配SQL片段的statement
        sqlElement(list, configuration.getDatabaseId());
    }
    // 再提取通用的SQL片段
    sqlElement(list, null);
}

private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        String databaseId = context.getStringAttribute("databaseId");
        String id = context.getStringAttribute("id");
        id = builderAssistant.applyCurrentNamespace(id, false);
        // 鉴别当前SQL片段是否匹配
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            sqlFragments.put(id, context);
        }
    }
}

可以发现处理逻辑是很简单的是吧!这里面的匹配 SQL 片段的逻辑还蛮有意思的,我们可以研究一下:

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    // 显式配置了需要databaseId,那就直接匹配
    if (requiredDatabaseId != null) {
        return requiredDatabaseId.equals(databaseId);
    }
    // 不需要databaseId,但这个SQL片段有声明,则一律不收
    if (databaseId != null) {
        return false;
    }
    // 还没有存过这条SQL片段,则直接收下
    if (!this.sqlFragments.containsKey(id)) {
        return true;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    // 已经存过了?拿出来看看是不是有databaseId,如果有,那就说明存在同id但没有配置databaseId的,不管了
    // (存在同id的情况下,有databaseId的优先级比没有的高)
    XNode context = this.sqlFragments.get(id);
    return context.getStringAttribute("databaseId") == null;
}

2.2.5 解析statement

最后一部分又是很复杂的了,它会解析 mapper.xml 中声明的 <select><insert><update><delete> 标签,并最终封装为一个一个的 **MappedStatement** 。有关 databaseId 的处理逻辑,还是跟 SQL 片段一样,小册不多重复,我们要关注的还是如何处理和解析这些 statement 的标签们:

2.2.5.1 解析的入口

解析 statement 标签,有两部分内容需要加载:如果在 MyBatis 全局配置文件中声明过 databaseId ,则这里解析时会检查 statement 标签中的 databaseId 并有针对性的加载;除此之外,所有情况下都会加载那些没有标注 databaseId 的标签,并封装为 MappedStatement

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, 
                builderAssistant, context, requiredDatabaseId);
        try {
            // 【复杂、困难】借助XMLStatementBuilder解析一个一个的statement标签
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            // statement解析失败,只会记录到Configuration中,但不会抛出异常
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

这里面首先遇到的,是之前先劝退各位的 XMLStatementBuilder ,它本身也跟 XMLMapperBuilder 差不多,都是用于解析 xml 文件、构建所需组件的建造器,只不过 XMLStatementBuilder 专注于解析一个 statement 标签( insert update delete select )内部的内容了(动态 SQL 也离不开 xml )。

下面我们进入 parseStatementNode 方法。方法比较长,小册仍然会拆分为几段来讲解。

2.2.5.2 databaseId的鉴别

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
    // ......

这段逻辑是非常简单的,它会先检查当前的这个 statement 是否与当前数据源对应的数据库一致,如果不匹配,则不会加载。

2.2.5.3 收集属性

String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

这一段的内容,是收集 statement 标签中定义的一些基础属性,以及判断这个 statement 是否为 DQL (即 select )。

2.2.5.4 include标签的支持

// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

接下来它会创建一个 XMLIncludeTransformer ,并应用于当前 statement 的子标签节点。我们知道,动态 SQL 是可以引用一些 SQL 片段的,而 MyBatis 底层支持 SQL 片段拼接的工具,就是这个 XMLIncludeTransformer 。它的内部实现比较复杂比较绕,主要是利用了递归解析的方式,将动态 SQL 中的 <include /> 标签转换为实际的 SQL 片段,如:

<select id="findAll">
    select <include refid="allFields" /> from tbl_department
</select>

<sql id="allFields">
    id, name, tel
</sql>

解析完毕后就是:

select id, name, tel from tbl_department

2.2.5.5 基础属性的处理

String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);

String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);

接下来的两段又是 statement 基础属性的一些处理,上面是处理传入参数的类型,下面是解析 SQL 语言的编写(一般我们都不会动它,都是使用默认的 xml 语言编写)。

2.2.5.6 selectKey标签的处理

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

下面的这一步,是处理 <select> 标签中的 <selectKey> 子标签,通常情况下我们用 <selectKey> 不算很多,一般是在自增主键的表插入数据时,获取自增值用。它的底层解析不算很复杂,我们来看看 processSelectKeyNodes 方法的实现:

private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    // 可能有多个selectKey
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    if (configuration.getDatabaseId() != null) {
        parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    removeSelectKeyNodes(selectKeyNodes);
}

从这个逻辑上看,<selectKey> 标签可以一次性用好几个,MyBatis 会把他们都收集起来一起解析。

解析的逻辑是下面的 parseSelectKeyNodes 方法:

private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, 
        LanguageDriver langDriver, String skRequiredDatabaseId) {
    for (XNode nodeToHandle : list) {
        // 每个selectKey也有自己的id
        String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        String databaseId = nodeToHandle.getStringAttribute("databaseId");
        if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
            // 实际的解析动作
            parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
        }
    }
}

这里它会循环每个 <selectKey> 标签了,会为它们赋予新的 id(追加一个 "!selectKey" 的后缀),以及判断它们的 databaseId (对,<selectKey> 也是可以针对不同的数据库分别配置的),当 databaseId 匹配,或者没有 databaseId 声明时,才会实际的解析这个标签。

而下面的解析动作,就没什么意思了,又是取值、创建 SQL 语句对象等等,小伙伴们简单扫一眼就 OK 了,小册在此不作展开。

private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, 
        LanguageDriver langDriver, String databaseId) {
    // 解析基础的属性,类型
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    StatementType statementType = StatementType.valueOf(nodeToHandle
             .getStringAttribute("statementType", StatementType.PREPARED.toString()));
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    // 该selectKey触发的时机,先于/晚于主体SQL执行
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    // defaults 默认值,MyBatis表示:这些你们都不用管了!
    boolean useCache = false;
    boolean resultOrdered = false;
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;

    // 解析SQL语句
    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;

    // selectKey的本质也是一个MappedStatement
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    // 将这个selectKey也放入MyBatis全局配置对象Configuration中
    id = builderAssistant.applyCurrentNamespace(id, false);
    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

2.2.5.7 构造SQL语句

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

这个环节就这一句话,但这句话也相当复杂了,我们暂且放下,下面单开一个 1.4 节讲解。

2.2.5.8 继续收集属性

StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
  resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");

下面又是好多好多的属性收集了,这些我们都快看腻了,扫一眼知道哟一下就可以了。

2.2.5.9 构建MappedStatement

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

最后的步骤,又是交给 MapperBuilderAssistant 干活了,而有了前面的源码经验,这个 addMappedStatement 方法的内部用的什么,我想不用我说各位也能猜得到吧,对,它还是用的建造器 MappedStatement.Builder 构建的 MappedStatement (所以 MyBatis 的源码,在一些流程上真的有非常大的相似之处,小伙伴们一定要多加留意)。

2.2.6 构造SQL

OK ,了解完整个 MappedStatement 的构建,接下来就剩下一个环节了:它里面的 SQL 是如何构建的呢?下面小册就解答这个问题。

2.2.6.1 LanguageDriver(org.apache.ibatis.scripting.LanguageDriver

这个 LanguageDriver 小册之前没有详细展开过,所以这里我们讲解一下。

MyBatis 的全局配置文件中有这么一个属性:**defaultScriptingLanguage** ,它可以指定动态 SQL 生成使用的默认脚本语言,但是为什么我们不知道呢?害,我们除了用 xml 编写动态 SQL 之外,也没见到过还用别的方式了,所以我们都默认这么干是唯一的了。

其实我们可以通过编写实现了 LanguageDriver 接口的自定义类,扩展动态 SQL 的编写方式,这是 MyBatis 留给我们的一个扩展点,只不过我们不会真的扩展罢了(本身动态 SQL 已经挺好用了)。

话说回来,这个 LanguageDriver 本身是个啥呢?从字面上翻译,它是一个脚本语言的驱动器,实际上它就是动态 SQL 编写的方式的解释器罢了。它拿到 statement 中我们编写的动态 SQL ,并按照它既定的解释规则,就可以生成 SQL 语句了。

有关解释器的概念,小伙伴们可以去翻看 GoF23 设计模式中的解释器模式。

MyBatis 本身给 LanguageDriver 接口留的实现也只有 xml 的方式,落地实现类是 XMLLanguageDriver ,所以下面我们的重点就放在 XMLLanguageDriver 上了。

2.2.6.2 createSqlSource

核心的处理逻辑是上面的那行 langDriver.createSqlSource ,它的实现又是交由另一个组件 XMLScriptBuilder 干活了:

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
}

是不是产生了一种感觉:特喵的咋动不动就甩直接给另外的组件干活呢?

我们先留意一下这个 XMLScriptBuilder 类。

XMLScriptBuilder的设计

这个 XMLScriptBuilder 又又又是继承自 BaseBuilder (这家伙的子类真的好多啊),很好理解,动态 SQL 要用 xml 写嘛,写完了要注册到 MyBatis 全局 Configuration 中,正好 BaseBuilder 中本身就集成了,继承一下还省事。

看一下它的构造吧,构造方法中有一段额外的逻辑值得我们留意一下:

public class XMLScriptBuilder extends BaseBuilder {

    private final XNode context;
    private boolean isDynamic;
    private final Class<?> parameterType;
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

    public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
        super(configuration);
        this.context = context;
        this.parameterType = parameterType;
        initNodeHandlerMap();
    }

这个 initNodeHandlerMap 方法的用意何为呢?我们进去看一下:

private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
}

咦,这 put 的这些东西,key 不就是动态 SQL 中可以使用的标签吗?合着这些标签的底层支持都是一堆 Handler ,在解析动态 SQL 时,遇到这些标签时,会使用相应的 Handler 去处理。至于怎么处理,下面我们马上就可以看到了。

解析SQL语句

我们往里跳转,来到 XMLScriptBuilderparseScriptNode 方法:

private boolean isDynamic;

public SqlSource parseScriptNode() {
    // 解析动态SQL标签
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    // 解析完毕后如果包含动态SQL,则封装为DynamicSqlSource
    if (isDynamic) {
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

这个方法实质就两步:解析动态 SQL 标签,封装 SqlSource 并返回。这里面复杂的是前一步 parseDynamicTags ,这一步会顺带着记录下 SQL 中是否包含动态 SQL 标签,以备下一步的 SqlSource 封装时作为判断依据。

解析动态SQL标签

继续往下看,复杂的动态 SQL 标签解析逻辑,这里面的逻辑略长,不过不难理解,小册给源码标注有注释,小伙伴们先看一下:

protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    // 提取出statement中所有的子节点(除了子标签之外,SQL明文也算)
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        // 当前子节点是普通文本,或者xml中的CDATA类,则认定为SQL文本
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            // 普通文本也有可能是动态的
            if (textSqlNode.isDynamic()) {
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
            // 如果是动态SQL标签,则解析标签
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(child, contents);
            // 并标注当前statement是动态SQL
            isDynamic = true;
        }
    }
    return new MixedSqlNode(contents);
}

仔细阅读以下这段源码,它解析的逻辑包含两部分:解析普通 SQL 文本,以及解析动态标签。

对于解析普通 SQL 文本的情况下,它判断的依据是:这段内容只有 SQL ,没有标签;这段内容是被 CDATA 包围的。符合这两种情况的文本会被认定为普通 SQL ,并直接添加到 contents 的那个 SQL 节点集合中。注意它中间还有一个是否为动态 SQL 的判断,可能会有小伙伴产生疑惑,普通文本 SQL 怎么可能会是动态 SQL 呢?来,我们回想一下 #{}${} 的区别,我们刚学习 MyBatis 的时候,知道的是 #{} 底层是占位符,而 ${} 会被直接替换为明文是吧,那 ${} 在生成 SQL 时,是不是就需要动态拼接了呀?所以这个地方,检查普通 SQL 文本是否为动态 SQL ,其实就是判断这段 SQL 中有没有 **${}** 表达式,如果有,则会认定当前 statement 中的 SQL 是动态 SQL 。

对于动态 SQL 标签的解析,它检查的是有没有 xml 的子标签,有的话就从上面初始化的那一堆 Handler 中找可以处理它的 Handler ,并处理它。处理完成后,标注一下当前 statement 中的 SQL 是动态 SQL 。

以此这样走下去,一个 statement 中的动态 SQL 就解析完毕了,小伙伴们可以自己编写几个。

解析完毕后,根据这个 SQL 语句中是否包含动态部分,决定包装为 DynamicSqlSource 还是 RawSqlSource (实际就是静态 SQL ),返回,SQL 解析完毕。

2.3 bindMapperForNamespace

接下来要执行的 bindMapperForNamespace 方法,本质上是为了 Mapper 接口动态代理而设计的,我们都知道,利用 Mapper 动态代理的特性,可以使得我们可以直接取 Mapper 接口,而不用操纵 SqlSession 的 API ,写那一堆复杂的 statementId ,而且也相对更容易维护代码了。

这个 bindMapperForNamespace 的方法,就是为这个特性做的支撑,我们来看看它的底层实现:

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            // 尝试加载namespace对应的类
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            // ignore, bound type is not required
        }
        // 加载到类了,并且之前没存过这个Mapper接口,那就存起来
        if (boundType != null && !configuration.hasMapper(boundType)) {
            // Spring may not know the real resource name so we set a flag
            // to prevent loading again this resource from the mapper interface
            // look at MapperAnnotationBuilder#loadXmlResource
            // Spring可能不知道真实的资源名称,因此设置了一个标志来防止再次从Mapper接口加载此资源
            configuration.addLoadedResource("namespace:" + namespace);
            configuration.addMapper(boundType);
        }
    }
}

嚯,这操作也忒简单了吧!直接把 namespace 拿来,用类加载去加载对应的类,如果加载到了,就把它存起来,完事;如果没加载到,那就当无事发生。是的,这本身的逻辑可以说是相当简单了,不过这里面有个细节,也就是源码中的几行单行注释:

Spring may not know the real resource name so we set a flag to prevent loading again this resource from the mapper interface.

Spring 可能不知道真实的资源名称,因此设置了一个标志来防止再次从 Mapper 接口加载此资源。

这个操作是图个啥呢?小册来解释一下原因。

在学习 MyBatis 基础的时候,讲到 Mapper 接口动态代理时,应该各位都记得有一个约定吧:在 MyBatis 全局配置文件中配置 mapper 时,如果使用包扫描的方式,扫描 Mapper 接口时,需要 Mapper 接口与对应的 mapper.xml 放在同一个目录下,且 Mapper 接口的名称要与 mapper.xml 的名称一致。这个约定的底层原理,是 Mapper 接口包扫描时,会自动寻找同目录下的同名称的 mapper.xml 文件并加载解析(核心代码可参照 org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#loadXmlResource )。但这个时候就有可能出现意外情况:如果我们既配置了 mapper.xml 的资源路径,又配置了 Mapper 接口包扫描,那岂不是要加载两边?这很明显不是很合理吧!所以 MyBatis 帮我们考虑了这一层,就在这里面加了一个额外的标识:每当解析到一个存在的 Mapper 接口时,会标记这个接口对应的 mapper.xml 文件已加载,这样即便又进行包扫描时读到了这个 Mapper 接口,当它要去加载 mapper.xml 时检查到已经加载过了,就不会再重复加载了。

2.4 重新处理不完整的元素

parse 方法的最后 3 个步骤其实都是干的同一类事情,那就是重新处理一下前面解析过程中保存的残缺不全的元素们:

parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();

我们以 resultMap 为例看一下里面的实现:

private void parsePendingResultMaps() {
    Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
    synchronized (incompleteResultMaps) {
        Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
        while (iter.hasNext()) {
            try {
                // 逐个提取,重新解析
                iter.next().resolve();
                iter.remove();
            } catch (IncompleteElementException e) {
                // ResultMap is still missing a resource...
            }
        }
    }
}

public ResultMap resolve() {
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}

注意这里提取出来的是一组 ResultMapResolver ,刚好呼应上面 2.3.3 节提到的那个看似多余的封装操作!可见这个 ResultMapResolver 并不是多余的做这么一步,通过 ResultMapResolver 这么一个中间层的传递,可以把一个 resultMap 中涉及到的所有定义信息都存起来,这样在重新处理的时候,可以直接把那些解析好的 resultMap 的信息都找回来,并直接让 **MapperBuilderAssistant** 再试一次

走完一遍迭代后,能正常解析的会从 incompleteResultMaps 中移除,剩余的还在里面继续呆着。当然这个时候再解析不了的 resultMap 也好,statement 也好,MyBatis 还没有彻底放弃它们。在 MyBatis 与 SpringFramework 的整合中,IOC 容器刷新完成后,会最后一次解析这些残缺不全的 resultMap 等等,这部分内容我们放到 MyBatis 整合 SpringFramework 的章节中再展开。

最后更新于