在上一讲,我们深入分析了MyBatis 初始化过程中对 mybatis-config.xml 全局配置文件的解析,详细介绍了其中每个标签的解析流程以及涉及的经典设计模式——构造者模式。这一讲我们就紧接着上一讲的内容,继续介绍 MyBatis 初始化流程,重点介绍Mapper.xml 配置文件的解析以及 SQL 语句的处理逻辑。

Mapper.xml 映射文件解析全流程

在上一讲分析 mybatis-config.xml 配置文件解析流程的时候我们看到,在 mybatis-config.xml 配置文件中可以定义多个 标签指定 Mapper 配置文件的地址,MyBatis 会为每个 Mapper.xml 映射文件创建一个 XMLMapperBuilder 实例完成解析。

与 XMLConfigBuilder 类似,XMLMapperBuilder也是具体构造者的角色,继承了 BaseBuilder 这个抽象类,解析 Mapper.xml 映射文件的入口是 XMLMapperBuilder.parse() 方法,其核心步骤如下:

执行 configurationElement() 方法解析整个Mapper.xml 映射文件的内容;

获取当前 Mapper.xml 映射文件指定的 Mapper 接口,并进行注册;

处理 configurationElement() 方法中解析失败的 标签;

处理 configurationElement() 方法中解析失败的 标签;

处理 configurationElement() 方法中解析失败的SQL 语句标签。

可以清晰地看到,configurationElement() 方法才是真正解析 Mapper.xml 映射文件的地方,其中定义了处理 Mapper.xml 映射文件的核心流程:

获取 标签中的 namespace 属性,同时会进行多种边界检查;

解析 标签;

解析 标签;

解析 标签;

解析 标签;

解析 标签中的select 语句定义,它会将当前列的值作为参数传入这个 select 语句。由于当前结果集可能查询出多行数据,那么可能就会导致 select 属性指定的 SQL 语句会执行多次,也就是著名的 N+1 问题。

columnPrefix(String 类型):当前标签的 columnPrefix 属性值,记录了表中列名的公共前缀。

resultSet(String 类型):当前标签的 resultSet 属性值。

lazy(boolean 类型):当前标签的fetchType 属性,表示是否延迟加载当前标签对应的列。

介绍完 ResultMapping 对象(即 标签下各个子标签的解析结果)之后,我们再来看 标签如何被解析。整个 标签最终会被解析成 ResultMap 对象,它与 ResultMapping 之间的映射关系如下图所示:

ResultMap 结构图

通过上图我们可以看出,ResultMap 中有四个集合与 ResultMapping 紧密相连。

resultMappings 集合,维护了整个 标签解析之后得到的全部映射关系,也就是全部 ResultMapping 对象。

idResultMappings 集合,维护了与唯一标识相关的映射,例如, 标签、 标签下的 子标签解析得到的 ResultMapping 对象。如果没有定义 等唯一性标签,则由 resultMappings 集合中全部映射关系来确定一条记录的唯一性,即 idResultMappings 集合与 resulMappings 集合相同。

constructorResultMappings 集合,维护了 标签下全部子标签定义的映射关系。

propertyResultMappings 集合,维护了不带 Constructor 标志的映射关系。

除了上述四个 ResultMapping 集合,ResultMap 中还维护了下列核心字段。

id(String 类型):当前 标签的 id 属性值。

type(Class 类型):当前 的 type 属性值。

mappedColumns(Set 类型):维护了所有映射关系中涉及的 column 属性值,也就是所有的列名(或别名)。

hasNestedResultMaps(boolean 类型):当前 标签是否嵌套了其他 标签,即这个映射关系中指定了 resultMap属性,且未指定 resultSet 属性。

hasNestedQueries(boolean 类型):当前 标签是否含有嵌套查询。也就是说,这个映射关系中是否指定了 select 属性。

autoMapping(Boolean 类型):当前 ResultMap 是否开启自动映射的功能。

discriminator(Discriminator 类型):对应 标签。

接下来我们开始深入分析 标签解析的流程。XMLMapperBuilder的resultMapElements() 方法负责解析 Mapper 配置文件中的全部 标签,其中会通过 resultMapElement() 方法解析单个 标签。

下面是 resultMapElement() 方法解析 标签的核心流程。

获取 标签的type 属性值,这个值表示结果集将被映射成 type 指定类型的对象。如果没有指定 type 属性的话,会找其他属性值,优先级依次是:type、ofType、resultType、javaType。在这一步中会确定映射得到的对象类型,这里支持别名转换。

解析 标签下的各个子标签,每个子标签都会生成一个ResultMapping 对象,这个 ResultMapping 对象会被添加到resultMappings 集合(List 类型)中暂存。这里会涉及 等子标签的解析。

获取 标签的id 属性,默认值会拼装所有父标签的id、value 或 property 属性值。

获取 标签的extends、autoMapping 等属性。

创建 ResultMapResolver 对象,ResultMapResolver 会根据上面解析到的ResultMappings 集合以及 标签的属性构造 ResultMap 对象,并将其添加到 Configuration.resultMaps 集合(StrictMap 类型)中。

(1)解析 标签

在 resultMapElement() 方法中获取到 id 属性和 type 属性值之后,会调用 buildResultMappingFromContext() 方法解析上述标签得到 ResultMapping 对象,其核心逻辑如下:

获取当前标签的property的属性值作为目标属性名称(如果 标签使用的是 name 属性);

获取 column、javaType、typeHandler、jdbcType、select 等一系列属性,与获取 property 属性的方式类似;

根据上面解析到的信息,调用 MapperBuilderAssistant.buildResultMapping() 方法创建 ResultMapping 对象。

正如 resultMapElement() 方法核心步骤描述的那样,经过解析得到 ResultMapping 对象集合之后,会记录到resultMappings 这个临时集合中,然后由 ResultMapResolver 调用 MapperBuilderAssistant.addResultMap() 方法创建 ResultMap 对象,将resultMappings 集合中的全部 ResultMapping 对象添加到其中,然后将ResultMap 对象记录到 Configuration.resultMaps 集合中。

下面是 MapperBuilderAssistant.addResultMap() 的具体实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public ResultMap addResultMap(
        String id,
        Class<?> type,
        String extend,
        Discriminator discriminator,
        List<ResultMapping> resultMappings,
        Boolean autoMapping) {
    // ResultMap的完整id是"namespace.id"的格式
    id = applyCurrentNamespace(id, false);
    // 获取被继承的ResultMap的完整id,也就是父ResultMap对象的完整id
    extend = applyCurrentNamespace(extend, true);
    if (extend != null) {  // 针对extend属性的处理
        // 检测Configuration.resultMaps集合中是否存在被继承的ResultMap对象
        if (!configuration.hasResultMap(extend)) {
            throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
        }
        // 获取需要被继承的ResultMap对象,也就是父ResultMap对象
        ResultMap resultMap = configuration.getResultMap(extend);
        // 获取父ResultMap对象中记录的ResultMapping集合
        List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
        // 删除需要覆盖的ResultMapping集合
        extendedResultMappings.removeAll(resultMappings);
        // 如果当前<resultMap>标签中定义了<constructor>标签,则不需要使用父ResultMap中记录
        // 的相应<constructor>标签,这里会将其对应的ResultMapping对象删除
        boolean declaresConstructor = false;
        for (ResultMapping resultMapping : resultMappings) {
            if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
                declaresConstructor = true;
                break;
            }
        }
        if (declaresConstructor) {
            extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
        }
        // 添加需要被继承下来的ResultMapping对象记录到resultMappings集合中
        resultMappings.addAll(extendedResultMappings);
    }
    // 创建ResultMap对象,并添加到Configuration.resultMaps集合中保存
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
            .discriminator(discriminator)
            .build();
    configuration.addResultMap(resultMap);
    return resultMap;
}

至于 标签的流程,是由XMLMapperBuilder 中的processConstructorElement() 方法实现,其中会先获取 标签的全部子标签,然后为每个标签添加 CONSTRUCTOR 标志(为每个 标签添加额外的ID标志),最后通过 buildResultMappingFromContext()方法创建 ResultMapping对象并记录到 resultMappings 集合中暂存,这些 ResultMapping 对象最终也会添加到前面介绍的ResultMap 对象。

(2)解析 标签

接下来,我们来介绍解析 标签的核心流程,两者解析的过程基本一致。前面介绍的 buildResultMappingFromContext() 方法不仅完成了 等标签的解析,还完成了 标签的解析,其中相关的代码片段如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
    ... // <association>标签中其他属性的解析与<result>、<id>标签类似,这里不再展开
    // 如果<association>标签没有指定resultMap属性,那么就是匿名嵌套映射,需要通过
    //  processNestedResultMappings()方法解析该匿名的嵌套映射
    String nestedResultMap = context.getStringAttribute("resultMap", () ->
            processNestedResultMappings(context, Collections.emptyList(), resultType));
    ... // <association>标签中其他属性的解析与<result>、<id>标签类似,这里不再展开
    // 根据上面解析到的属性值,创建ResultMapping对象
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}

这里的 processNestedResultMappings() 方法会递归执行resultMapElement() 方法解析 标签和 标签指定的匿名嵌套映射,得到一个完整的ResultMap 对象,并添加到Configuration.resultMaps集合中。

(3)解析 标签

最后一个要介绍的是 标签的解析过程,我们将 标签与 标签配合使用,根据结果集中某列的值改变映射行为。从 resultMapElement() 方法的逻辑我们可以看出, 标签是由 processDiscriminatorElement() 方法专门进行解析的,具体实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) {
    // 从<discriminator>标签中解析column、javaType、jdbcType、typeHandler四个属性的逻辑非常简单,这里将这部分代码省略
    Map<String, String> discriminatorMap = new HashMap<>();
    // 解析<discriminator>标签的<case>子标签
    for (XNode caseChild : context.getChildren()) {
        String value = caseChild.getStringAttribute("value");
        // 通过前面介绍的processNestedResultMappings()方法,解析<case>标签,
        // 创建相应的嵌套ResultMap对象
        String resultMap = caseChild.getStringAttribute("resultMap",
                processNestedResultMappings(caseChild, resultMappings, resultType));
        // 记录该列值与对应选择的ResultMap的Id
        discriminatorMap.put(value, resultMap);
    }
    // 创建Discriminator对象
    return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}

SQL 语句解析全流程

在 Mapper.xml 映射文件中,除了上面介绍的标签之外,还有一类比较重要的标签,那就是