Mybatis源码学习(26)-resultMap元素的使用及解析过程

一、用法

  关于结果映射元素<resultMap>的结构及用法,详细可以参考《Mybatis官方文档-结果映射》。下面摘了一些重要概念及其用法,如下:

  resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的长达数千行的代码。ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。

  你已经见过简单映射语句的示例了,但并没有显式指定 resultMap。比如:

<select id="selectUsers" resultType="map">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

  上述语句只是简单地将所有的列映射到 HashMap 的键上,这由 resultType 属性指定。虽然在大部分情况下都够用,但是 HashMap 不是一个很好的领域模型。你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 对象)作为领域模型。MyBatis 对两者都提供了支持。JavaBean 可以被映射到 ResultSet,就像映射到 HashMap 一样简单。如下所示,其中User就是JavaBean类的别名。

<select id="selectUsers" resultType="User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

  这些情况下,MyBatis 会在幕后自动创建一个 ResultMap,再基于属性名来映射列到 JavaBean 的属性上。如果列名和属性名没有精确匹配,可以在 SELECT 语句中对列使用别名(这是一个基本的 SQL 特性)来匹配标签。比如:

<select id="selectUsers" resultType="User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from some_table
  where id = #{id}
</select>

  ResultMap 最优秀的地方在于,虽然你已经对它相当了解了,但是根本就不需要显式地用到他们。 上面这些简单的示例根本不需要下面这些繁琐的配置。 但出于示范的原因,让我们来看看最后一个示例中,如果使用外部的 resultMap 会怎样,这也是解决列名不匹配的另外一种方式。

<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>

  而在引用它的语句中使用 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:

<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from some_table
  where id = #{id}
</select>
二、<resultMap>元素的结构
1、<resultMap>元素的属性

  根据mybatis-3-mapper.dtd文件的定义,<resultMap>元素结构如下:

<!ELEMENT resultMap (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ATTLIST resultMap
id CDATA #REQUIRED
type CDATA #REQUIRED
extends CDATA #IMPLIED
autoMapping (true|false) #IMPLIED
>

其中,<resultMap>元素有如下四个属性:

  • id 当前命名空间中的一个唯一标识,用于标识一个结果映射。
  • type 类的完全限定名, 或者一个类型别名
  • autoMapping 如果设置这个属性,MyBatis将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。
  • extends 结果映射的继承关系,即表示继承的结果映射。

<resultMap>元素包含的子元素有constructor,id,result,association,collection, discriminator等,下面分别分析其结构:

2、<constructor>子元素

  用于在实例化类时,注入结果到构造方法中。<constructor>子元素又可以包括idArg和arg两个子元素。其中,
idArg表示 ID参数;标记出作为 ID 的结果可以帮助提高整体性能;arg表示将被注入到构造方法的一个普通结果。

子元素idArg和arg可以有的属性及其含义如下:

  • javaType
    一个 Java 类的完全限定名,或一个类型别名。 如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。
  • column
    数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。
  • jdbcType
    JDBC 类型。只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可能存在空值的列指定这个类型。
  • typeHandler
    类型处理器。使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。
  • select
    用于加载复杂类型属性的映射语句的 ID,它会从 column 属性中指定的列检索数据,作为参数传递给此 select 语句。
  • resultMap
    结果映射的 ID,可以将嵌套的结果集映射到一个合适的对象树中。 它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet。这样的 ResultSet 将会将包含重复或部分数据重复的结果集。为了将结果集正确地映射到嵌套的对象树中,MyBatis 允许你 “串联”结果映射,以便解决嵌套结果集的问题。
  • name
    构造方法形参的名字。从 3.4.3 版本开始,通过指定具体的参数名,你可以以任意顺序写入 arg 元素。
  • columnPrefix
    当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。
<!ELEMENT constructor (idArg*,arg*)>
<!ELEMENT idArg EMPTY>
<!ATTLIST idArg
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
select CDATA #IMPLIED
resultMap CDATA #IMPLIED
name CDATA #IMPLIED
columnPrefix CDATA #IMPLIED
>

<!ELEMENT arg EMPTY>
<!ATTLIST arg
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
select CDATA #IMPLIED
resultMap CDATA #IMPLIED
name CDATA #IMPLIED
columnPrefix CDATA #IMPLIED
>
3、<id>子元素

  <id>元素表示一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能。<id>元素没有子元素,但是可以包括如下property、javaType、column、jdbcType、typeHandler等属性。其中,property属性表示映射到列结果的字段或属性。如果用来匹配的 JavaBean 存在给定名字的属性,那么它将会被使用。否则 MyBatis 将会寻找给定名称的字段。 无论是哪一种情形,你都可以使用通常的点式分隔形式进行复杂属性导航。 比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。其他属性字段和上述提到的<idArg >和<arg>两个元素中的属性含义是一样的。

<!ELEMENT id EMPTY>
<!ATTLIST id
property CDATA #IMPLIED
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
4、<result>子元素

  注入到字段或 JavaBean 属性的普通结果。和<id>元素功能类似,只是<result>元素表示的是普通字段。属性和<id>元素中的属性一样,这里不再赘述。

<!ELEMENT result EMPTY>
<!ATTLIST result
property CDATA #IMPLIED
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
5、<association>子元素

  一个复杂类型的关联;许多结果将包装成这种类型。其中包含的子元素和<resultMap>元素一样,这里不再重复;包含的属性有:property 、column 、javaType 、jdbcType 、select 、resultMap 、typeHandler 、columnPrefix 、autoMapping 、notNullColumn 、resultSet 、foreignColumn 、fetchType ,其中前九个已经在前面分析了且含有是一样的,现在分析其他属性,如下:

  • notNullColumn
    默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。 你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列非空时才创建一个子对象。可以使用逗号分隔来指定多个列。默认值:未设置(unset)。

  • resultSet
    关联的多结果集。从版本 3.2.3 开始,MyBatis 提供了另一种解决 N+1 查询问题的方法。某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。 我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。

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

    <select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
      {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
    </select>
    
  • foreignColumn
    指定外键对应的列名,指定的列将与父类型中 column 的给出的列进行匹配。

  • fetchType
    可选的。有效值为 lazy 和 eager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值。

结构定义:

<!ELEMENT association (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ATTLIST association
property CDATA #REQUIRED
column CDATA #IMPLIED
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
select CDATA #IMPLIED
resultMap CDATA #IMPLIED
typeHandler CDATA #IMPLIED
notNullColumn CDATA #IMPLIED
columnPrefix CDATA #IMPLIED
resultSet CDATA #IMPLIED
foreignColumn CDATA #IMPLIED
autoMapping (true|false) #IMPLIED
fetchType (lazy|eager) #IMPLIED
>
6、<collection>子元素

  一个复杂类型的集合。集合元素和关联元素<association>几乎是一样的,它们相似的程度之高,以致于没有必要再介绍集合元素的相似部分。区别在于:association:处理一对一关联(has one);collection处理一对多关联(has many)。

7、<discriminator>子元素

  鉴别器。有时候,一个数据库查询可能会返回多个不同的结果集(但总体上还是有一定的联系的)。 鉴别器(discriminator)元素就是被设计来应对这种情况的,另外也能处理其它情况,例如类的继承层次结构。 鉴别器的概念很好理解——它很像 Java 语言中的 switch 语句。

  一个鉴别器的定义需要指定 column 和 javaType 属性。column 指定了 MyBatis 查询被比较值的地方。 而 javaType 用来确保使用正确的相等测试(虽然很多情况下字符串的相等测试都可以工作)。例如:

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultMap="carResult"/>
    <case value="2" resultMap="truckResult"/>
    <case value="3" resultMap="vanResult"/>
    <case value="4" resultMap="suvResult"/>
  </discriminator>
</resultMap>

结构定义:

<!ELEMENT discriminator (case+)>
<!ATTLIST discriminator
column CDATA #IMPLIED
javaType CDATA #REQUIRED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>

<!ELEMENT case (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ATTLIST case
value CDATA #REQUIRED
resultMap CDATA #IMPLIED
resultType CDATA #IMPLIED
>
三、<resultMap>元素解析过程

  从《Mapper映射文件中个元素的解析过程》中,我们可以知道,在XMLMapperBuilder类的configurationElement()方法开始解析元素,然后通过resultMapElements()方法及其重载方法实现<resultMap>元素解析过程。具体逻辑如下:

1、解析入口

  首先,在configurationElement()方法中调用resultMapElements()方法开始进行<resultMap>元素解析。在XML配置文件中,可能存在多个<resultMap>节点,然后通过for循环,调用重载的resultMapElements()方法,来实现解析每一个<resultMap>元素节点。

//XMLMapperBuilder.java
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());
  }
2、resultMapElement()方法

  然后,在重载方法resultMapElement(XNode resultMapNode, List additionalResultMappings)中开始进行单个<resultMap>元素的解析过程。代码如下:

//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");
    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));
      }
    }
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }

  通过分析上述方法,我们可以知道,处理逻辑如下:

  首先,解析<resultMap>元素的属性id、type、extends、autoMapping,并把type对应的字符串,解析成对应的Class对象。
  然后,通过for循环解析<resultMap>元素下的子节点。如果是constructor节点,通过processConstructorElement()方法解析,如果是discriminator节点,通过processDiscriminatorElement()方法解析。其他子节点通过buildResultMappingFromContext()方法解析。
  最后,通过ResultMapResolver对象的resolve()方法,构建对应的ResultMap对象。在这方法中,其实还是通过MapperBuilderAssistant类的addResultMap()方法实现。

3、processConstructorElement()方法

  在前面提到了该方法是用来解析constructor节点的。该方法通过解析constructor节点下的子元素,并通过buildResultMappingFromContext()方法构建ResultMapping对象,添加到resultMappings上。buildResultMappingFromContext()方法后续再分析。方法如下:

//XMLMapperBuilder.java
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));
    }
  }
4、processDiscriminatorElement()方法

  解析Discriminator节点。首先解析该节点的属性,然后解析包含的case子元素,最后通过MapperBuilderAssistant辅助类的buildDiscriminator()方法构建Discriminator对象。

//XMLMapperBuilder.java
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>();
    for (XNode caseChild : context.getChildren()) {
      String value = caseChild.getStringAttribute("value");
      String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
      discriminatorMap.put(value, resultMap);
    }
    return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
  }
5、buildResultMappingFromContext()方法

  该方法是解析除了Discriminator节点之外所有节点的基础方法。首先也是解析节点的属性信息,然后通过processNestedResultMappings()方法解析内嵌的resultMap节点,最后通过MapperBuilderAssistant辅助类的buildResultMapping()构建ResultMapping对象。具体代码如下:

//XMLMapperBuilder.java
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");
    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);
  }

解析内嵌元素的方法processNestedResultMappings(),代码如下:

//XMLMapperBuilder.java
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;
  }

通过上面的代码,可以了解到,在processNestedResultMappings方法中,最终还是通过resultMapElement()方法进行解析内嵌的元素,即又回到了上面分析的resultMapElement()方法中。

总之,通过上述分析的解析过程,把配置文件中的元素都转换成对应的Java对象,然后再通过builderAssistant.buildResultMapping()方法,构建最终的ResultMapping对象。

发布了71 篇原创文章 · 获赞 3 · 访问量 5283

猜你喜欢

转载自blog.csdn.net/hou_ge/article/details/103715647