MyBatis 解析xml配置的mapper - litter-fish/ReadSource GitHub Wiki

SQL 语句解析时序图

对于XML格式的mapper文件解析委托给 XMLMapperBuilder 进行解析

创建XMLMapperBuilder对象

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    // 创建Mapper 构建助手
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
}

// 创建Mapper 构建助手
public MapperBuilderAssistant(Configuration configuration, String resource) {
    super(configuration);
    ErrorContext.instance().resource(resource);
    this.resource = resource;
}

// super
public BaseBuilder(Configuration configuration) {
    //
    this.configuration = configuration;
    // 设置类型别名注册器
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    // 类型处理器注册器
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}

委托 XMLMapperBuilder 进行解析

// --XMLMapperBuilder
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        // 解析mapper子节点
        configurationElement(parser.evalNode("/mapper"));
        // 解析完后将文件加入configuration对象已经解析的属性集合中
        configuration.addLoadedResource(resource);
        // 绑定命名空间
        bindMapperForNamespace();
    }

    // 解析待定的ResultMap
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

解析 mapper 标签

// --XMLMapperBuilder
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);
        // 解析对其他命名空间缓存配置的引用。
        cacheRefElement(context.evalNode("cache-ref"));
        // 解析对给定命名空间的缓存配置。
        cacheElement(context.evalNode("cache"));
        // 已被废弃!老式风格的参数映射。更好的办法是使用内联参数,此元素可能在将来被移除。
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        // 解析描述如何从数据库结果集中来加载对象
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 解析 可被其他语句引用的可重用语句块。
        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);
    }
}

解析cache-ref子节点

在多个命名空间中共享相同的缓存配置和实例。

解析时序图

// --XMLMapperBuilder
/**
 * 在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。
 * @param context
 */
private void cacheRefElement(XNode context) {
    if (context != null) {
        // 将当前处理的名称空间加入configuration对象的cacheRefMap map集合中
        configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
        // 创建 cache-ref 解析器 CacheRefResolver
        CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
        try {
            // 委托CacheRefResolver解析 cache-ref节点
            cacheRefResolver.resolveCacheRef();
        } catch (IncompleteElementException e) {
            /**
             * 缓存参考因为通过namespace指向其他的缓存。所以会出现第一次解析的时候指向的缓存还不存在的情况,所以需要在所有的mapper文件加载完成后进行二次处理,
             * 不仅仅是缓存参考,其他的CRUD也一样。所以在XMLMapperBuilder.configuration中有很多的incompleteXXX,这种设计模式类似于JVM GC中的mark and sweep,
             * 标记、然后处理。所以当捕获到IncompleteElementException异常时,没有终止执行,而是将指向的缓存不存在的cacheRefResolver添加到
             * configuration.incompleteCacheRef中。
             */
            configuration.addIncompleteCacheRef(cacheRefResolver);
        }
    }
}

首先会将当前处理的名称空间加入configuration对象的cacheRefMap map集合中, 接着创建 cache-ref 解析器 CacheRefResolver, 最后委托CacheRefResolver解析 cache-ref节点。

// org/apache/ibatis/builder/CacheRefResolver.java
public Cache resolveCacheRef() {
    return assistant.useCacheRef(cacheRefNamespace);
}

委托mapper构建助手进行处理

// org/apache/ibatis/builder/MapperBuilderAssistant.java
public Cache useCacheRef(String namespace) {
    if (namespace == null) {
        throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
        unresolvedCacheRef = true;
        // 首先查询caches集合是否存在其引用
        Cache cache = configuration.getCache(namespace);
        if (cache == null) {
            // 不存在的抛异常,cacheRefElement中会加入incompleteCacheRefs集合中进行二次解析
            throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
        }
        currentCache = cache;
        unresolvedCacheRef = false;
        return cache;
    } catch (IllegalArgumentException e) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
}

对于解析过程中如果发现Cache中不存在对应的引用,将抛出IncompleteElementException异常,然后加入configuration对象的incompleteCacheRef集合中

解析cache子节点

默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

cache解析时序图

源码解释:

// --XMLMapperBuilder
/**
 * 默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
 * 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。
 *
 * 映射语句文件中的所有 select 语句的结果将会被缓存。
 * 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
 * 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
 * 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
 * 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
 * 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
 *
 * 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。??????
 */
private void cacheElement(XNode context) throws Exception {
  if (context != null) {

    // 默认情况下,mybatis使用的是永久缓存PerpetualCache
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    // 获取清除策略
    /**
     * LRU – 最近最少使用:移除最长时间不被使用的对象。
     * FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
     * SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
     * WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
     */
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);

    // flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
    Long flushInterval = context.getLongAttribute("flushInterval");

    // size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024
    Integer size = context.getIntAttribute("size");
    // readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。
    // 而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    // 调用builderAssistant.useNewCache构建缓存
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

首先获取节点的属性,type、eviction,通过属性名获取相应的class对象。 接着委托Mapper创建助手进行缓存的创建。 缓存的构建 builderAssistant.useNewCache

// --MapperBuilderAssistant
public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    boolean blocking,
    Properties props) {
    // 构建Cache 实例
  Cache cache = new CacheBuilder(currentNamespace)
      .implementation(valueOrDefault(typeClass, PerpetualCache.class)) // 二级缓存默认使用:PerpetualCache
      .addDecorator(valueOrDefault(evictionClass, LruCache.class)) // 默认装饰器:LruCache
      .clearInterval(flushInterval)
      .size(size)
      .readWrite(readWrite)
      .blocking(blocking)
      .properties(props)
      .build();
  // 将二级缓存配置设置到configuration实例中
  configuration.addCache(cache);
  currentCache = cache;
  return cache;
}

cache构建模式的分析

构建Cache 实例

// --CacheBuilder
public Cache build() {
    // 设置实现类及装饰类
    setDefaultImplementations();
    // 创建实现类
    Cache cache = newBaseCacheInstance(implementation, id);
    // 设置缓存的属性
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
            // 给缓存加上装饰器
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 给缓存加上一些功能
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}

设置实现类及装饰类

private void setDefaultImplementations() {
  if (implementation == null) {
    // 设置永久缓存
    implementation = PerpetualCache.class;
    // 如果无装饰类则添加LruCache 清除策略 装饰类
    if (decorators.isEmpty()) {
      decorators.add(LruCache.class);
    }
  }
}

反射方式创建缓存实现类

// org/apache/ibatis/mapping/CacheBuilder.java
private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
    Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
    try {
        return cacheConstructor.newInstance(id);
    } catch (Exception e) {
        throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
    }
}

反射方式给缓存添加装饰器

private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
    Constructor<? extends Cache> cacheConstructor = getCacheDecoratorConstructor(cacheClass);
    try {
        return cacheConstructor.newInstance(base);
    } catch (Exception e) {
        throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e);
    }
}

给缓存增加装饰类

private Cache setStandardDecorators(Cache cache) {
  try {
    MetaObject metaCache = SystemMetaObject.forObject(cache);
    if (size != null && metaCache.hasSetter("size")) {
      metaCache.setValue("size", size);
    }
    if (clearInterval != null) {
      // 加强缓存的定时刷新功能
      cache = new ScheduledCache(cache);
      ((ScheduledCache) cache).setClearInterval(clearInterval);
    }
    if (readWrite) {
      // 加强缓存的可序列化操作
      cache = new SerializedCache(cache);
    }
    // 加强缓存的日志功能
    cache = new LoggingCache(cache);
    // 加强缓存的同步功能,解决线程安全问题
    cache = new SynchronizedCache(cache);
    if (blocking) {
      // 加强缓存的阻塞功能
      cache = new BlockingCache(cache);
    }
    return cache;
  } catch (Exception e) {
    throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
  }
}

最终构造的缓存实例

结果集 resultMap 解析

ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。

复杂的结果映射例子

<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <association property="author" javaType="Author"/>
    <collection property="comments" ofType="Comment">
      <id property="id" column="comment_id"/>
    </collection>
    <collection property="tags" ofType="Tag" >
      <id property="id" column="tag_id"/>
    </collection>
    <discriminator javaType="int" column="draft">
      <case value="1" resultType="DraftPost"/>
    </discriminator>
  </collection>
</resultMap>

结果映射(resultMap)

  • constructor - 用于在实例化类时,注入结果到构造方法中 idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能 arg - 将被注入到构造方法的一个普通结果

  • id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能

  • result – 注入到字段或 JavaBean 属性的普通结果

  • association – 一个复杂类型的关联;许多结果将包装成这种类型 嵌套结果映射 – 关联本身可以是一个 resultMap 元素,或者从别处引用一个

  • collection – 一个复杂类型的集合 嵌套结果映射 – 集合本身可以是一个 resultMap 元素,或者从别处引用一个

  • discriminator – 使用结果值来决定使用哪个 resultMap case – 基于某些值的结果映射 嵌套结果映射 – case 本身可以是一个 resultMap 元素,因此可以具有相同的结构和元素,或者从别处引用一个

resultMap解析时序图

遍历所有的 resultMap 节点进行解析

// --XMLMapperBuilder
private void resultMapElements(List<XNode> list) throws Exception {
    // 遍历所有的 resultMap 节点进行解析
    for (XNode resultMapNode : list) {
        try {
            resultMapElement(resultMapNode);
        } catch (IncompleteElementException e) {}
    }
}

resultMap 节点解析

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

委托重载方法解析 resultMap 节点

// org/apache/ibatis/builder/xml/XMLMapperBuilder.java
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());

    // 	当前命名空间中的一个唯一标识,用于标识一个结果映射。
    String id = resultMapNode.getStringAttribute("id",
            resultMapNode.getValueBasedIdentifier());

    // 类的完全限定名, 或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。
    String type = resultMapNode.getStringAttribute("type",
            resultMapNode.getStringAttribute("ofType",
                    resultMapNode.getStringAttribute("resultType",
                            resultMapNode.getStringAttribute("javaType"))));

    String extend = resultMapNode.getStringAttribute("extends");

    // 如果设置这个属性,MyBatis将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。
    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();
    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));
        }
    }
    // 所有的子节点都被解析到resultMappings中, 在解析完整个resultMap中的所有子元素之后,调用ResultMapResolver进行解析
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
        return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

首先获取resultMap节点的属性, 接着获取子节点,遍历子节点进行解析。

解析 resultMap 的子节点构造器 constructor, 将结果放入 resultMappings 中

resultMap 的子节点构造器 constructor的解析时序图:

// --XMLMapperBuilder
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
    List<XNode> argChildren = resultChild.getChildren();
    /**
     * <constructor>
     *   <idArg column="blog_id" javaType="int"/>
     *   <arg column="blog_name" javaType="string"/>
     * </constructor>
     */
    for (XNode argChild : argChildren) {
        List<ResultFlag> flags = new ArrayList<ResultFlag>();
        flags.add(ResultFlag.CONSTRUCTOR);
        if ("idArg".equals(argChild.getName())) {
            // 子节点名称为idArg的节点表示ID需要特殊处理
            flags.add(ResultFlag.ID);
        }
        // 将constructor子节点 构造 ResultMapping 成实例
        resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
    }
}

首先根据节点名称,区别是ID节点还是非ID节点,接着将constructor子节点 构造 ResultMapping 成实例

构造 ResultMapping 实例, 将结果放入 resultMappings 中

// --XMLMapperBuilder
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    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");

    // resultMap中可以包含association或collection复合类型,这些复合类型可以使用外部定义的公用resultMap或者内嵌resultMap,
    // 所以这里的处理逻辑是如果有resultMap就获取resultMap,如果没有,那就动态生成一个。如果自动生成的话,他的resultMap id通过调用XNode.getValueBasedIdentifier()来获得
    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);
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}

首先获取参数的属性配置,然后使用这些属性构造一个ResultMapping对象,其中参数属性resultMap可以包含association或collection复合类型

嵌套resultMap的解析

// --XMLMapperBuilder
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception {
    // 嵌套resultMap的解析
    if ("association".equals(context.getName())
            || "collection".equals(context.getName())
            || "case".equals(context.getName())) {
        if (context.getStringAttribute("select") == null) {
            // 未配置复杂类型属性的映射语句的 ID,进行递归解析ResultMap节点
            ResultMap resultMap = resultMapElement(context, resultMappings);
            return resultMap.getId();
        }
    }
    return null;
}

子节点 discriminator 鉴别器的解析

子节点 discriminator 鉴别器的解析时序图

子节点 discriminator 鉴别器的解析, 将结果放入 resultMappings 中

// --XMLMapperBuilder
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()) {
        String value = caseChild.getStringAttribute("value");
        // 递归解析case子节点
        String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
        discriminatorMap.put(value, resultMap);
    }
    // 创建Discriminator实例
    return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}

委托ResultMapResolver进行ResultMap的封装

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

configuration配置中加入ResultMap解析对象

// --MapperBuilderAssistant
public ResultMap addResultMap(
        String id,
        Class<?> type,
        String extend,
        Discriminator discriminator,
        List<ResultMapping> resultMappings,
        Boolean autoMapping) {

    // 将id/extend填充为完整模式,也就是带命名空间前缀,true不需要和当前resultMap所在的namespace相同,比如extend和cache,否则只能是当前的namespace
    id = applyCurrentNamespace(id, false);
    extend = applyCurrentNamespace(extend, true);

    if (extend != null) {
        // 首先检查继承的resultMap是否已存在,如果不存在则标记为incomplete,会进行二次处理
        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());
        // 剔除所继承的resultMap里已经在当前resultMap
        extendedResultMappings.removeAll(resultMappings);
        // Remove parent constructor if this resultMap declares a constructor.
        // 如果本resultMap已经包含了构造器,则剔除继承的resultMap里面的构造器
        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();
                }
            }
        }

        // 都处理完成之后,将继承的resultMap里面剩下那部分不重复的resultMap子元素添加到当前的resultMap中,
        // 所以这个addResultMap方法的用途在于启动时就创建了完整的resultMap,这样运行时就不需要去检查继承的映射和构造器,有利于性能提升。
        resultMappings.addAll(extendedResultMappings);
    }
    // 构造ResultMap对象
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
            .discriminator(discriminator)
            .build();
    configuration.addResultMap(resultMap);
    return resultMap;
}

ResultMap对象属性

public class ResultMap {
    private Configuration configuration;

    // resultMap的id属性
    private String id;

    // resultMap的type属性,有可能是alias
    private Class<?> type;

    // resultMap下的所有节点
    private List<ResultMapping> resultMappings;

    // resultMap下的id节点比如<id property="id" column="user_id" />
    private List<ResultMapping> idResultMappings;

    // resultMap下的构造器节点<constructor>
    private List<ResultMapping> constructorResultMappings;

    // resultMap下的property节点比如<result property="password" column="hashed_password"/>
    private List<ResultMapping> propertyResultMappings;

    // 映射的列名
    private Set<String> mappedColumns;

    // 映射的javaBean属性名,所有映射不管是id、构造器或者普通的 TODO >?????
    private Set<String> mappedProperties;

    // 鉴别器
    private Discriminator discriminator;

    // 是否有嵌套的resultMap比如association或者collection
    private boolean hasNestedResultMaps;

    // 是否有嵌套的查询,也就是select属性
    private boolean hasNestedQueries;

    // autoMapping属性,这个属性会覆盖全局的属性autoMappingBehavior
    private Boolean autoMapping;
}

ResultMapping属性

public class ResultMapping {

  private Configuration configuration;
  private String property;
  private String column;
  private Class<?> javaType;
  private JdbcType jdbcType;
  private TypeHandler<?> typeHandler;
  private String nestedResultMapId;
  private String nestedQueryId;
  private Set<String> notNullColumns;
  private String columnPrefix;
  private List<ResultFlag> flags;
  private List<ResultMapping> composites;
  private String resultSet;
  private String foreignColumn;
  private boolean lazy;
}

构造ResultMap对象

// --ResultMap
public ResultMap build() {
    if (resultMap.id == null) {
        throw new IllegalArgumentException("ResultMaps must have an id");
    }
    resultMap.mappedColumns = new HashSet<String>();
    resultMap.mappedProperties = new HashSet<String>();
    resultMap.idResultMappings = new ArrayList<ResultMapping>();
    resultMap.constructorResultMappings = new ArrayList<ResultMapping>();
    resultMap.propertyResultMappings = new ArrayList<ResultMapping>();
    final List<String> constructorArgNames = new ArrayList<String>();

    for (ResultMapping resultMapping : resultMap.resultMappings) {

        // 判断是否有嵌套查询, nestedQueryId是在buildResultMappingFromContext的时候通过读取节点的select属性得到的
        resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;

        // 判断是否嵌套了association或者collection, nestedResultMapId是在buildResultMappingFromContext的时候通过读取节点的resultMap属性得到的
        // 或者内嵌resultMap的时候自动计算得到的。
        // 注:这里的resultSet没有地方set进来,DTD中也没有看到,不确定是不是有意预留的,但是association/collection的子元素中倒是有声明
        resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps
                || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);

        // 获取column属性, 包括复合列,复合列是在org.apache.ibatis.builder.MapperBuilderAssistant.parseCompositeColumnName(String)中解析的。
        // 所有的数据库列都被按顺序添加到resultMap.mappedColumns中
        final String column = resultMapping.getColumn();
        if (column != null) {
            resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
        } else if (resultMapping.isCompositeResult()) {
            for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
                final String compositeColumn = compositeResultMapping.getColumn();
                if (compositeColumn != null) {
                    resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
                }
            }
        }

        // 所有映射的属性都被按顺序添加到resultMap.mappedProperties中,ID单独存储
        final String property = resultMapping.getProperty();
        if(property != null) {
            resultMap.mappedProperties.add(property);
        }

        // 所有映射的构造器被按顺序添加到resultMap.constructorResultMappings
        // 如果本元素具有CONSTRUCTOR标记,则添加到构造函数参数列表,否则添加到普通属性映射列表resultMap.propertyResultMappings
        if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
            resultMap.constructorResultMappings.add(resultMapping);
            if (resultMapping.getProperty() != null) {
                constructorArgNames.add(resultMapping.getProperty());
            }
        } else {
            resultMap.propertyResultMappings.add(resultMapping);
        }

        // 如果本元素具有ID标记, 则添加到ID映射列表resultMap.idResultMappings
        if (resultMapping.getFlags().contains(ResultFlag.ID)) {
            resultMap.idResultMappings.add(resultMapping);
        }
    }

    // 如果没有声明ID属性,就把所有属性都作为ID属性
    if (resultMap.idResultMappings.isEmpty()) {
        resultMap.idResultMappings.addAll(resultMap.resultMappings);
    }

    // 根据声明的构造器参数名和类型,反射声明的类,检查其中是否包含对应参数名和类型的构造器,如果不存在匹配的构造器,就抛出运行时异常,这是为了确保运行时不会出现异常
    if (!constructorArgNames.isEmpty()) {
        final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
        if (actualArgNames == null) {
            throw new BuilderException("Error in result map '" + resultMap.id
                    + "'. Failed to find a constructor in '"
                    + resultMap.getType().getName() + "' by arg names " + constructorArgNames
                    + ". There might be more info in debug log.");
        }
        // 构造器参数排序
        Collections.sort(resultMap.constructorResultMappings, new Comparator<ResultMapping>() {
            @Override
            public int compare(ResultMapping o1, ResultMapping o2) {
                int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
                int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
                return paramIdx1 - paramIdx2;
            }
        });
    }
    // lock down collections
    // 为了避免用于无意或者有意事后修改resultMap的内部结构, 克隆一个不可修改的集合提供给用户
    resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
    resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
    resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
    resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
    resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
    return resultMap;
}

association元素

嵌套结果映射 – 关联本身可以是一个 resultMap 元素,或者从别处引用一个 关联(association)元素处理“有一个”类型的关系。 MyBatis 有两种不同的方式加载关联:

  • 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
  • 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。

元素的属性

属性              描述
property          映射到列结果的字段或属性。
javaType          一个 Java 类的完全限定名,或一个类型别名
jdbcType          JDBC 类型
typeHandler       类型处理器实现类的完全限定名,或者是类型别名
  1. 关联的嵌套 Select 查询
属性              描述
column            数据库中的列名,或者是列的别名。使用 column="{prop1=col1,prop2=col2}" 来指定多个传递给嵌套 Select 查询语句的列名
select            用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。
fetchType         有效值为 lazy 和 eager,指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled

示例:

<resultMap id="blogResult" type="Blog">
  <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectAuthor" resultType="Author">
  SELECT * FROM AUTHOR WHERE ID = #{id}
</select>

“N+1 查询问题”: 执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。

解决方法:延迟加载,将大量语句同时运行的开销分散开来。

  1. 关联的嵌套结果映射

元素的属性

属性              描述
resultMap         结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。
columnPrefix      当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。 
notNullColumn     默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。 你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列非空时才创建一个子对                    象。可以使用逗号分隔来指定多个列。默认值:未设置(unset)。
autoMapping       如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。

示例:

<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author"
    resultMap="authorResult" />
  <association property="coAuthor"
    resultMap="authorResult"
    columnPrefix="co_" />
</resultMap>

collection元素

可以使用该元素表示:private List posts; 映射嵌套结果集合到一个 List 中,可以使用集合元素。 和关联元素一样,我们可以使用嵌套 Select 查询,或基于连接的嵌套结果映射集合。

  1. 集合的嵌套 Select 查询
<resultMap id="blogResult" type="Blog">
  <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectPostsForBlog" resultType="Post">
  SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

“ofType” 属性。这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。 表示:“posts 是一个存储 Post 的 ArrayList 集合”

  1. 集合的嵌套结果映射
<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>

<resultMap id="blogPostResult" type="Post">
  <id property="id" column="id"/>
  <result property="subject" column="subject"/>
  <result property="body" column="body"/>
</resultMap>

集合的多结果集(ResultSet)

属性              描述
column            当使用多个结果集时,该属性指定结果集中用于与 foreignColumn 匹配的列(多个列名以逗号隔开),以识别关系中的父类型与子类型。
foreignColumn      指定外键对应的列名,指定的列将与父类型中 column 的给出的列进行匹配。
resultSet         指定用于加载复杂类型的结果集名字。

通过这种方式可以解决N+1问题: 存储过程执行下面的查询并返回两个结果集。第一个结果集会返回博客(Blog)的结果,第二个则返回作者(Author)的结果。

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM AUTHOR WHERE ID = #{id}

在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。

<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
  {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>

可以指定使用 “authors” 结果集的数据来填充 “author” 关联:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <result property="email" column="email"/>
    <result property="bio" column="bio"/>
  </association>
</resultMap>

最终ResultMap对象

第一层ResultMap

处理后的ResultMapping内存结构

sql子节点的解析

这个元素可以被用来定义可重用的 SQL 代码段,这些 SQL 代码可以被包含在其他语句中。它可以(在加载的时候)被静态地设置参数。 在不同的包含语句中可以设置不同的值到参数占位符上。比如:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

这个 SQL 片段可以被包含在其他语句中,例如:

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

属性值也可以被用在 include 元素的 refid 属性里或 include 元素的内部语句中,例如:

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

解析 可被其他语句引用的可重用语句SQL

// --XMLMapperBuilder
private void sqlElement(List<XNode> list) throws Exception {
    if (configuration.getDatabaseId() != null) {
        sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
}

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
    for (XNode context : list) {
        String databaseId = context.getStringAttribute("databaseId");
        String id = context.getStringAttribute("id");
        id = builderAssistant.applyCurrentNamespace(id, false);
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            sqlFragments.put(id, context);
        }
    }
}
⚠️ **GitHub.com Fallback** ⚠️