mybatis resultMap 加载过程详解

本文详细解析MyBatis的resultMap标签及其子标签的用途与配置方法,帮助理解resultMap如何映射数据库查询结果到Java对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

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<XNode> 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.<ResultMapping> 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 的子标签又有哪些,其他标签都包含哪些呢?

名称类型
constructorconstructor
discriminatordiscriminator
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<ResultFlag> flags = new ArrayList<ResultFlag>();
    // 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<ResultFlag> 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.<ResultMapping> 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")
    Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends 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) {
      // 获得  <id column="id" property="id"/> 节点的 typeHandler 的值,本例未定义,结果为 null
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    //根据typeHandlerClass获得对象,结果为 null
    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    // 嵌套查询时解析为多个字段,例如: <association property="title" column="title={title},content={content}" ....
    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);
      // 组合列参数不可更改 <association column="title={title},content={content}" ....
      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<ResultMapping> resultMappings) throws Exception {
    List<XNode> argChildren = resultChild.getChildren();
    for (XNode argChild : argChildren) {
      List<ResultFlag> flags = new ArrayList<ResultFlag>();
      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" >
        <!-- 直接使用对象,在result为对象赋值 -->
        <association property="content"  javaType="string" >
            <result column="content" />
        </association>
    <!-- 使用 select 引用的查询结果,可以通过column传参 -->
    <!-- 多参 key 是select引用需要用到变量,value是这边查询到的值 {id=id,content=content} -->
        <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" >
        <!-- 直接使用集合, 那么bean的集合必须初始化 -->
        <collection property="contentList"  javaType="string" >
            <result column="content"  />
        </collection>
        <!-- 通过select 获取的集合一定不会为null.  所以bean的定义可以不用初始化 -->
        <collection property="contentListSelect" column="id" select="selectContentList"  javaType="list"  />
    </resultMap>
public class Blog {

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

association | collection 流程分析

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

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

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

嵌套标签解析

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

嵌套多列处理

 List<ResultMapping> 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 标签,会导致查询结果错误-->
 <!-- result标签没有column会导致装配ResultMap对象失败,抛出异常 IllegalStateException -->
            <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<ResultMapping> 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")
    Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends 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,
      Class<? extends TypeHandler<?>> typeHandler,
      Map<String, String> discriminatorMap) {
      // 将辨别器节点构建成一个 ResultMapping
    ResultMapping resultMapping = buildResultMapping(
        resultType,
        null,
        column,
        javaType,
        jdbcType,
        null,
        null,
        null,
        null,
        typeHandler,
        new ArrayList<ResultFlag>(),
        null,
        null,
        false);
    Map<String, String> namespaceDiscriminatorMap = new HashMap<String, String>();
    //构建辨别器的所有子节点
    for (Map.Entry<String, String> 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<ResultMapping> 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<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(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<ResultMapping> 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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值