mybatis resultMap 加载过程详解

目录

  • 流程分析
    • 遍历resultMap根节点
    • 重载的resultMapElement
  • 子标签解析
    • 其他流程-普通标签 id|result 标签
      • buildResultMappingFromContext
      • buildResultMapping
    • 构造流程 constructor 标签
    • 其他流程-嵌套标签 - association 标签 | collection 标签
      • association 用法
      • collection 用法
      • association | collection 流程分析
      • 嵌套标签解析
      • 嵌套多列处理
    • 辨别器流程 discriminator 标签
      • 基本使用
      • 理论介绍
      • 源码分析
  • 根标签解析
  • 详解
    • 构造器多参详解
      • 方案3 idea设置
      • 方案4 黑科技
  • 总结

mybatis 有9大标签,分别是:
- cache-ref
- cache
- parameterMap
- resultMap
- sql
- select
- insert
- update
- delete

其中 parameterMap 已被官网标记为废弃

cache-refcache 在这个电商横行,分布式肆虐的年代,也不使用 mybatis 缓存了

那么常用的只有其中6种
- resultMap
- sql
- select
- insert
- update
- delete

本节我们针对resultMap进行详细分析

分析结束之后我们可以解答内心的一些疑问:
1. resultMap 有哪些子标签,都用来干什么
2. collection 标签为什么有时候要自己初始化(new ArrayList<>())

流程分析

先从 XMLMapperBuilder 的 configurationElement 说起, 这里存放着9大标签的解析

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      // cache-ref 标签解析
      cacheRefElement(context.evalNode("cache-ref"));
      // cache 标签解析
      cacheElement(context.evalNode("cache"));
      // parameterMap 标签解析
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // resultMap 标签解析
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // sql 标签解析
      sqlElement(context.evalNodes("/mapper/sql"));
      // select|insert|update|delete 标签解析
      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);
    }
  }

遍历resultMap根节点

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

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

首先进入第一道开胃菜:
1. resultMapElements, 它遍历了mapper下面所有的resultMap标签,然后每个都进行 resultMapElement 处理
2. resultMapElement 传入一个空的集合对象,调用重载

resultMapElement 为什么要传入一个空的集合对象呢?

那是因为resultMap标签可以嵌套,但是根节点就是空的

重载的resultMapElement

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
    // 保存当前上下文,用于异常信息回溯
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 获得 id 标签
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    // 依次获取默认值 type=>ofType=>resultType>javaType 
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    // 获得 继承 resultMap 的 id 
    String extend = resultMapNode.getStringAttribute("extends");
    // 获得 是否自动映射
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    Class typeClass = resolveClass(type);
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
    resultMappings.addAll(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    // 遍历子节点,依次解析为 ResultMapping 对象,并添加到集合 resultMappings
    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<ResultFlag>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      // 解析验证,最终添加到 configuration
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

看似很多,实际上就是两步
1. 遍历解析子标签为 ResultMapping 对象
2. 解析根标签 为一个ResultMap并添加到configuration

子标签解析

这里又分三种情况
1. constructor 子标签解析
2. discriminator 子标签解析
3. 其他标签解析

那resultMap 的子标签又有哪些,其他标签都包含哪些呢?

名称 类型
constructor constructor
discriminator discriminator
id 其他
result 其他
association 其他->嵌套
collection 其他->嵌套

其他流程-普通标签 id|result 标签

Xml 例子

<resultMap id="BlogMap" type="com.aya.mapper.Blog">
    <id column="id" property="id"/>
resultMap>

(id|result|association|collection)标签解析流程

    List flags = new ArrayList();
    // id 标签加入 flags, 其他的就不加入这个标签
    if ("id".equals(resultChild.getName())) {
      flags.add(ResultFlag.ID);
    }
    resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));

这是最简单最常用的一种解析方式.
1. 添加标识 ID
2. 使用 buildResultMappingFromContext 构建成一个 ResultMapping 添加到集合 resultMappings

buildResultMappingFromContext

private ResultMapping buildResultMappingFromContext(XNode context, Class> resultType, List flags) throws Exception {
    String property;
    // constructor 标签用name当做属性
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
      property = context.getStringAttribute("name");
    } else {
    // 普通标签 标签用property当做属性
      property = context.getStringAttribute("property");
    }
    // 废话开始
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    // processNestedResultMappings 嵌套解析在 association|collection 讲解
    String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, Collections. emptyList()));
    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"));
    Class javaTypeClass = resolveClass(javaType);
    @SuppressWarnings("unchecked")
    Classextends TypeHandler> typeHandlerClass = (Classextends TypeHandler>) resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    // 废话结束
    //构建 ResultMapping
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  }

对于简单的id|result标签解析而言,这里面大都是将xml解析成具体的数据

这里要明白两点
1. 每一个 id|result|association|collection 标签,都对应着一个 ResultMapping 对象
2. ResultMapping 是通过builderAssistant.buildResultMapping 构建的

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 的值,本例未定义,结果为 null
    Class javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    //根据typeHandlerClass获得对象,结果为 null
    TypeHandler typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    // 嵌套查询时解析为多个字段,例如: 
    List<ResultMapping> 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<ResultFlag>() : flags)
        .composites(composites)
        .notNullColumns(parseMultipleColumnNames(notNullColumn))
        .columnPrefix(columnPrefix)
        .foreignColumn(foreignColumn)
        .lazy(lazy)
        .build();
  }

这里终于找到终点了

ResultMapping.Builder 是一个很标准的建造者模式。 符合建造者模式的核心思想。
1. 必要的参数使用构造方法传入
2. 可选的设置使用 withXXX 设置
3. build 之后对象不可变

在build 的时候,做了一个默认的类型解析处理

    public ResultMapping build() {
      // lock down collections
      // 内置标识 不可更改
      resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
      // 组合列参数不可更改 
      resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
      // 设置默认的类型处理器
      resolveTypeHandler();
      // 验证嵌套的信息有效
      validate();
      // 返回构建成功的 resultMapping
      // resultMapping 是成员变量
      return resultMapping;
    }


    private void resolveTypeHandler() {
      // 用户没有在标签定义 typeHandler时,使用mybatis默认的typeHandler
      if (resultMapping.typeHandler == null && resultMapping.javaType != null) {
        Configuration configuration = resultMapping.configuration;
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        resultMapping.typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType);
      }
    }

构造流程 constructor 标签

一个参数只要加入 javaType 就可以匹配到具体的构造函数了,不加javaType时,默认匹配 ctor(Object) 的构造

    <resultMap id="BlogMap" type="com.aya.mapper.Blog"  >
        <constructor>
            <idArg column="pid" javaType="integer"/>
        constructor>
    resultMap>

这里走流程 if ("constructor".equals(resultChild.getName())) 的代码块,分析 processConstructorElement 的内部

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

将 constructor 的每个子标签构建成 ResultMapping 对象,添加到 resultMappings

也就是两个步骤
1. 添加标识 ResultFlag.CONSTRUCTOR ,构建 ResultMapping
2. 添加子标签 ,每个 ResultMapping 都有标识ResultFlag.CONSTRUCTOR

其他流程-嵌套标签 - association 标签 | collection 标签

association 用法

association 是一个对象的解析器,有两种用法

    <select id="selectContent" resultType="string">
        SELECT content FROM blog WHERE id = #{id}
    select>
    <resultMap id="BlogMap" type="com.aya.mapper.Blog" >
        
        <association property="content"  javaType="string" >
            <result column="content" />
        association>
    
    
        <association property="contentSelect" column="id" select="selectContent"  javaType="string" />
    resultMap>
public class Blog {

    private String content;

    private String contentSelect;
    //省略 getter , setter
}

collection 用法

collection 是一个集合的解析器,有两种用法

    <select id="selectContentList" resultType="string">
        SELECT content FROM blog WHERE id = #{id}
    select>
    <resultMap id="BlogMap" type="com.aya.mapper.Blog" >
        
        <collection property="contentList"  javaType="string" >
            <result column="content"  />
        collection>
        
        <collection property="contentListSelect" column="id" select="selectContentList"  javaType="list"  />
    resultMap>
public class Blog {

    public Blog() {
    }
    // 必须初始化
    private List contentList = new ArrayList<>();
    // 可以不用初始化
    private List contentListSelect;
    //省略 getter , setter
}

association | collection 流程分析

这里走的流程是其他标签解析的流程,但是比普通的标签有额外的两个方法要详细说明

在前面讲 buildResultMappingFromContext 的分析的时候,在进行 resultMap 获取是,有一个嵌套的处理。

就是专门用来处理具有嵌套效果的标签的, 而association | collection 刚好就是嵌套标签。

嵌套标签解析

  String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, Collections. emptyList()));

嵌套多列处理

 List composites = parseCompositeColumnName(column);

嵌套标签解析

  private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception {
    if ("association".equals(context.getName())
        || "collection".equals(context.getName())
        || "case".equals(context.getName())) {
      if (context.getStringAttribute("select") == null) {
        ResultMap resultMap = resultMapElement(context, resultMappings);
        return resultMap.getId();
      }
    }
    return null;
  }

这里做的就是当 association|collection|case 标签没有属性select的时候,递归解析子标签

嵌套多列处理

  private List<ResultMapping> parseCompositeColumnName(String columnName) {
    List<ResultMapping> composites = new ArrayList<ResultMapping>();
    if (columnName != null && (columnName.indexOf('=') > -1 || columnName.indexOf(',') > -1)) {
      StringTokenizer parser = new StringTokenizer(columnName, "{}=, ", false);
      //例如 column="{columnKeyA=columnValueA,columnKeyB=columnValueB}"  分两次遍历,添加到composites
      while (parser.hasMoreTokens()) {
        String property = parser.nextToken();
        String column = parser.nextToken();
        ResultMapping complexResultMapping = new ResultMapping.Builder(
            configuration, property, column, configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build();
        composites.add(complexResultMapping);
      }
    }
    return composites;
  }

这里就是来解析标签中 column="{columnKeyA=columnValueA,columnKeyB=columnValueB}" 部分的代码

装配成ResultMapping集合,在最后build里面的validate进行验证

validate 验证嵌套

// Issue #4 and GH #39: column is optional only in nested resultmaps but not in the rest
      if (resultMapping.nestedResultMapId == null && resultMapping.column == null && resultMapping.composites.isEmpty()) {
        throw new IllegalStateException("Mapping is missing column attribute for property " + resultMapping.property);
      }

也就是说,你不可以写成以下形式

<collection property="contentList"  javaType="string" >
 
 
            <result  />
collection>

辨别器流程 discriminator 标签

基本使用

    <resultMap id="BlogMap" type="com.aya.mapper.Blog" >
        <result column="pid" property="id"/>
        <discriminator javaType="string" column="pid" >
            <case  value="1" resultType="com.aya.mapper.Blog">
                <result column="ptitle" property="title"/>
            case>
            <case value="2" resultType="com.aya.mapper.Blog">
                <result column="pcontent" property="title"/>
            case>
        discriminator>
    resultMap>
    
    <select id="selectAll" resultMap="BlogMap"    >
        select id as pid,title as ptitle,content as pcontent from blog
    select>

实体类

public class Blog {

    private String id;
    private String title;
    //省略 getter , setter
}

查询结果

[Blog{id='1', title='标题'}, Blog{id='2', title='content2'}, Blog{id='3', title='null'}]

理论介绍

discriminator 是一个辨别器. 用switch...case的方式决定让整个resultMap返回什么类型

也就是说,加载的时候只能是某个动态对象,执行查询操作后才知道实际的返回类型.

因为是switch...case,所以不能加入条件判断

  discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);

discriminator 的判断的地方可以发现, 如果有discriminator,那么有且仅有一个.

接下来就分析Discriminator对象的创建过程

源码分析

private Discriminator processDiscriminatorElement(XNode context, Class> resultType, List resultMappings) throws Exception {
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String typeHandler = context.getStringAttribute("typeHandler");
    Class javaTypeClass = resolveClass(javaType);
    @SuppressWarnings("unchecked")
    Classextends TypeHandler> typeHandlerClass = (Classextends TypeHandler>) resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    Map<String, String> discriminatorMap = new HashMap<String, String>();
    //遍历case节点
    for (XNode caseChild : context.getChildren()) {
     // case 的value
      String value = caseChild.getStringAttribute("value");
      //processNestedResultMappings 解析嵌套的ResultMapping,并返回创建的名称
      String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
      discriminatorMap.put(value, resultMap);
    }
    // 构建 Discriminator
    return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
  }
  1. 每个value都对应着一个ResultMapping名称,在根据查询结果进行转换
  2. 使用构建助手构建辨别器
public Discriminator buildDiscriminator(
      Class resultType,
      String column,
      Class javaType,
      JdbcType jdbcType,
      Classextends TypeHandler> typeHandler,
      Map<String, String> discriminatorMap) {
      // 将辨别器节点构建成一个 ResultMapping
    ResultMapping resultMapping = buildResultMapping(
        resultType,
        null,
        column,
        javaType,
        jdbcType,
        null,
        null,
        null,
        null,
        typeHandler,
        new ArrayList(),
        null,
        null,
        false);
    Map namespaceDiscriminatorMap = new HashMap();
    //构建辨别器的所有子节点
    for (Map.Entry e : discriminatorMap.entrySet()) {
      String resultMap = e.getValue();
      // 使用将简写改为全称
      resultMap = applyCurrentNamespace(resultMap, true);
      namespaceDiscriminatorMap.put(e.getKey(), resultMap);
    }
    // 用建造者模式创建构建器
    return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build();

这里就通过 Discriminator 的建造者创建了对象.

Discriminator 是一个根据结果动态选择返回类型的一个标签,单纯的分析解析过程,意义不大

根标签解析

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

通过上一层的 builderAssistant.addResultMap ,实际上添加到哪里去了呢?

 public ResultMap addResultMap(
      String id,
      Class type,
      String extend,
      Discriminator discriminator,
      List resultMappings,
      Boolean autoMapping) {
      // 简称改为全称
    id = applyCurrentNamespace(id, false);
    //继承
    extend = applyCurrentNamespace(extend, true);
    //继承解析
    if (extend != null) {
      if (!configuration.hasResultMap(extend)) {
        throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
      }
      ResultMap resultMap = configuration.getResultMap(extend);
      List extendedResultMappings = new ArrayList(resultMap.getResultMappings());
      extendedResultMappings.removeAll(resultMappings);
      // Remove parent constructor if this resultMap declares a constructor.
      boolean declaresConstructor = false;
      for (ResultMapping resultMapping : resultMappings) {
        if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
          declaresConstructor = true;
          break;
        }
      }
      if (declaresConstructor) {
        Iterator extendedResultMappingsIter = extendedResultMappings.iterator();
        while (extendedResultMappingsIter.hasNext()) {
          if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {
            extendedResultMappingsIter.remove();
          }
        }
      }
      resultMappings.addAll(extendedResultMappings);
    }
    //ResultMap.Builder 构建 ResultMap.  根标签 resultMap 
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
        .discriminator(discriminator)
        .build();
    // 将根标签解析的对象resultMap添加到configuration
    configuration.addResultMap(resultMap);
    return resultMap;
  }

实际上那么多的解析从宏观上就只有两步
1. 将resultMap的所有子标签解析成ResultMapping对象
2. 将resultMap标签构建成ResultMap对象添加到configuration

详解

构造器多参详解

    <resultMap id="BlogMap" type="com.aya.mapper.Blog"  >
        <constructor>
            <idArg column="pid" name="id" javaType="integer"/>
            <arg column="ptitle" name="title" javaType="string"/>
        constructor>
    resultMap>
public class Blog implements Serializable{
    private Integer id;
    private String title;
    public Blog(Integer id, String title) {
        this.id = id;
        this.title = title;
    }
}

以上面的代码为例,查询时在创建ResultMap时,抛出异常

方案3 idea设置

打开File => Settings

依次选择 Buld,Execution,Deployment => Compiler => Java Compiler

设置 Additional command line parameters: -parameters

方案4 黑科技

切勿使用

黑科技: 既然不不开启特性,又想对应,那我就满足你的原理

    <resultMap id="BlogMap" type="com.aya.mapper.Blog"  >
        <constructor>
            <idArg column="pid" name="arg0" javaType="integer"/>
            <arg column="ptitle" name="arg1" javaType="string"/>
        constructor>
    resultMap>

总结

  1. resultMap 有6个子标签
  2. select属性关联集合不会为null

你可能感兴趣的:(mybatis,源码分析)