MyBatis3源码深度解析(二十五)级联映射与关联查询(二)级联映射的实现原理

文章目录

    • 前言
    • 10.2 MyBatis级联映射的实现原理
      • 10.2.1 ResultMap详解
      • 10.2.2 ResultMap解析过程
      • 10.2.3 级联映射的实现原理
        • 10.2.3.1 handleRowValuesForSimpleResultMap()
        • 10.2.3.2 handleRowValuesForNestedResultMap()

前言

上一节【MyBatis3源码深度解析(二十四)级联映射与关联查询(一)级联映射的使用】通过编写一个测试案例,学习了如何使用<resultMap>标签实现MyBatis的一对多、一对一级联映射。

本节来研究一下MyBatis级联映射的实现原理。先说明一下本节使用的测试案例代码:

<!--UserMapper.xml-->
<resultMap id="fullUser" type="User"><id column="user_id" property="userId"/><result column="name" property="name"/><!--<result column="age" property="age"/>--><!--<result column="phone" property="phone"/>--><result column="birthday" property="birthday"/><collection property="orderList"select="com.star.mybatis.mapper.OrderMapper.listOrderByUserId"ofType="Order"javaType="List"column="user_id"></collection>
</resultMap>

age和phone属性被注释掉了,是故意的,下面分析会用到。

@Test
public void testOne2ManyQuery() {User user = userMapper.getFullUserById(1);System.out.println(user.toString());
}

10.2 MyBatis级联映射的实现原理

10.2.1 ResultMap详解

MyBatis是一个半自动化的ORM框架,可以将数据库中的记录转换为Java实体对象,但是Java实体属性通常采用驼峰命名法,而数据库表字段习惯采用下划线分割命名法,因此需要用户指定Java实体属性与数据库表字段之间的映射关系。

Mapper配置中的<resultMap>标签,就用于建立Java实体属性与数据库表字段之间的映射关系,例如本节的测试案例代码。

测试案例的配置中,每个ResultMap有一个全局唯一的ID,即<resultMap>标签的id属性;还会通过type属性指定与哪一个Java实体进行映射。

在<resultMap>标签中,需要使用<id>或<result>标签配置具体的某个表字段与Java实体属性之间的映射关系。数据库主键通常使用<id>标签建立映射关系,普通数据库字段则使用<result>标签。

除了属性映射,ResultMap还支持构造器映射,即<constructor>标签,例如:

<!--UserMapper.xml-->
<resultMap id="UserMap" type="User"><constructor><idArg column="user_id" javaType="Integer"/><arg column="name" javaType="String"/></constructor><result column="age" property="age"/><result column="phone" property="phone"/><result column="birthday" property="birthday"/>
</resultMap>

使用构造器映射的前提是,Java实体中存在与之相对应的构造方法。<idArg>标签用于配置数据库主键的映射,<arg>标签用于配置数据库普通字段的映射。

最后,总结一下<resultMap>标签的各个子标签的作用:

  • <id>:用于配置数据库主键的映射,标记出主键,提高整体性能。
  • <result>:用于配置数据库普通字段的映射。
  • <collection>:用于配置一对多关联映射,可以关联一个外部的查询Mapper或者配置一个嵌套的ResultMap。
  • <association>:用于配置一对一关联映射,可以关联一个外部的查询Mapper或者配置一个嵌套的ResultMap。
  • <discriminator>:用于配置根据字段值使用不同的ResultMap。该标签有一个子标签<case>,用于枚举字段值对应的ResultMap,类似于Java中的switch语法。
  • <constructor>:用于建立构造器映射。它有两个子标签,<idArg>标签用于配置数据库主键的映射,标记出主键,提高整体性能;<arg>标签用于配置数据库普通字段的映射。

10.2.2 ResultMap解析过程

MyBatis在启动时,所有配置信息都会被转换为Java对象,通过<resultMap>标签配置的结果集映射信息将会被转换为ResultMap对象。 其定义如下:

源码1org.apache.ibatis.mapping.ResultMappublic class ResultMap {private Configuration configuration;// <resultMap>标签的id属性private String id;// <resultMap>标签的type属性,指定与数据库表建立映射关系的Java实体private Class<?> type;// <result>标签配置的映射信息private List<ResultMapping> resultMappings;// <id>标签配置的主键映射信息private List<ResultMapping> idResultMappings;// <constructor>标签配置的构造器映射信息private List<ResultMapping> constructorResultMappings;// <result>标签配置的结果集映射信息private List<ResultMapping> propertyResultMappings;// 存放所有映射的数据库字段信息private Set<String> mappedColumns;// 存放所有映射的属性信息private Set<String> mappedProperties;// <discriminator>标签配置的鉴别器信息private Discriminator discriminator;// 是否有嵌套的<resultMap>private boolean hasNestedResultMaps;// 是否存在嵌套查询private boolean hasNestedQueries;// <resultMap>标签的autoMapping属性,是否自动映射private Boolean autoMapping;// ......
}

ResultMap类中定义的属性的含义如 源码1 中的注释所示。其中有几个属性需要单独解释下:

  • mappedColumns:用于存放所有映射的数据库字段信息。当使用columnPrefix属性配置了前缀时,MyBatis会对mappedColumns属性进行遍历,为所有数据库字段追加columnPrefix属性配置的前缀。
  • hasNestedResultMaps:该属性用于标识是否有嵌套的ResultMap。当使用<association>或<collection>标签,并以JOIN子句的方式配置一对一或一对多级联映射时,<association>或<collection>标签就相当于一个嵌套的ResultMap,此时hasNestedResultMaps属性为true。
  • hasNestedQueries:该属性用于标识是否有嵌套查询。当使用<association>或<collection>标签,并以外部Mapper的方式配置一对一或一对多级联映射时,<association>或<collection>标签存在嵌套查询,此时hasNestedResultMaps属性为true。
  • autoMapping:该标签用于标识是否开启自动映射。为true时,即使未使用<id>或<result>标签配置映射字段,MyBatis也会自动对这些字段进行映射。

MyBatis的Mapper配置信息的解析都是通过XMLMapperBuilder类完成的,该类提供了一个parse()方法,用于解析Mapper中的所有配置信息。

源码2org.apache.ibatis.builder.xml.XMLMapperBuilderpublic void parse() {if (!configuration.isResourceLoaded(resource)) {// 调用XPathParser的evalNode()方法获取根节点对应的XNode对象// 在调用configurationElement()方法解析该XNode对象configurationElement(parser.evalNode("/mapper"));// ......
}

由 源码2 可知,在XMLMapperBuilder类的parse()方法中,会调用configurationElement()方法解析<mapper>标签对应的XNode对象。

源码3org.apache.ibatis.builder.xml.XMLMapperBuilderprivate void configurationElement(XNode context) {try {// 获取和配置命名空间String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.isEmpty()) {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"));buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} // catch ...
}

由 源码3 可知,在XMLMapperBuilder类的configurationElement()方法中,会逐个解析各种标签,其中就有调用resultMapElements()方法解析<resultMap>标签。

源码4org.apache.ibatis.builder.xml.XMLMapperBuilderprivate void resultMapElements(List<XNode> list) {// 对全部<resultMap>标签进行遍历for (XNode resultMapNode : list) {try {resultMapElement(resultMapNode);} catch (IncompleteElementException e) {// ignore, it will be retried}}
}private ResultMap resultMapElement(XNode resultMapNode) {return resultMapElement(resultMapNode, Collections.emptyList(), null);
}private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings,Class<?> enclosingType) {ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());// 获取<resultMap>标签的属性,按照type→ofType→resultType→javaType的顺序获取// 如果type属性为空,则获取ofType属性,以此类推String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType",resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType"))));Class<?> typeClass = resolveClass(type);if (typeClass == null) {typeClass = inheritEnclosingType(resultMapNode, enclosingType);}Discriminator discriminator = null;List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);// 获取<resultMap>标签的子标签List<XNode> resultChildren = resultMapNode.getChildren();for (XNode resultChild : resultChildren) {if ("constructor".equals(resultChild.getName())) {// 处理<constructor>标签processConstructorElement(resultChild, typeClass, resultMappings);} else if ("discriminator".equals(resultChild.getName())) {// 处理<discriminator>标签discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);} else {// 处理其他标签List<ResultFlag> flags = new ArrayList<>();if ("id".equals(resultChild.getName())) {flags.add(ResultFlag.ID);}// 将各标签转换为ResultMapping对象并添加到resultMappings集合中resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));}}// 获取id、extend、autoMapping属性String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());String extend = resultMapNode.getStringAttribute("extends");Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");// 构造ResultMapResolver对象ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator,resultMappings, autoMapping);try {// 调用ResultMapResolver对象的resolve()方法return resultMapResolver.resolve();} catch (IncompleteElementException e) {configuration.addIncompleteResultMap(resultMapResolver);throw e;}
}

由 源码4 可知,在XMLMapperBuilder类的resultMapElements()方法中,会使用for循环语句对全部<resultMap>标签进行遍历,每一个<resultMap>标签均调用resultMapElement()方法。

resultMapElement()方法中,会获取<resultMap>标签的所有属性信息,对<id>、<constructor>、<discriminator>等标签进行解析,接着创建一个ResultMapResolver对象,调用ResultMapResolver对象的resolve()方法返回一个ResultMap对象。

源码5org.apache.ibatis.builder.ResultMapResolverpublic class ResultMapResolver {private final MapperBuilderAssistant assistant;private final String id;private final Class<?> type;private final String extend;private final Discriminator discriminator;private final List<ResultMapping> resultMappings;private final Boolean autoMapping;public ResultMapResolver(MapperBuilderAssistant assistant, String id, Class<?> type, String extend,Discriminator discriminator, List<ResultMapping> resultMappings, Boolean autoMapping) {this.assistant = assistant;this.id = id;this.type = type;this.extend = extend;this.discriminator = discriminator;this.resultMappings = resultMappings;this.autoMapping = autoMapping;}public ResultMap resolve() {return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings,this.autoMapping);}}

由 源码5 可知,在ResultMapResolver的resolve()方法中,会调用MapperBuilderAssistant对象的addResultMap()方法创建ResultMap对象。

源码6org.apache.ibatis.builder.MapperBuilderAssistantpublic 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) {// 继承了其他ResultMap的情况 ......}// 通过建造者模式创建ResultMap对象ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping).discriminator(discriminator).build();// 将ResultMap对象设置到Configuration对象中configuration.addResultMap(resultMap);return resultMap;
}

由 源码6 可知,在MapperBuilderAssistant对象的addResultMap()方法中,会通过建造者模式创建ResultMap对象,并将ResultMap对象设置到Configuration对象中。

借助Debug工具,可以查看测试案例的<resultMap>标签的解析结果:

由图可知,数据库字段与Java实体属性的映射关系封装在一个ResultMapping对象中。重点关注一下orderList属性,其对应的ResultMapping对象信息如下,其中<collection>标签的select属性被封装在ResultMapping对象nestedQueryId属性中:

10.2.3 级联映射的实现原理

默认情况下,MyBatis会选择PreparedStatement操作数据库,因此在执行测试案例中的getFullUserById()查询操作时,会调用PreparedStatementHandler对象的query()方法。

源码7org.apache.ibatis.executor.statement.PreparedStatementHandler@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {// 执行查询操作PreparedStatement ps = (PreparedStatement) statement;ps.execute();// 处理结果集return resultSetHandler.handleResultSets(ps);
}

通过Debug工具,可以发现query()方法被调用了两次,第一次的查询语句是select * from user where user_id = 1,第二次的查询语句是select * from `order` where user_id = 1

这和上一节的分析结果是一致的,在接下来的分析中也证实了这种结果。

由 源码7 可知,在PreparedStatementHandler对象的query()方法中,会在完成查询操作后,调用ResultSetHandler对象的handleResultSets()方法处理结果集。

ResultSetHandler接口只有一个实现类,即DefaultResultSetHandler类。

源码8org.apache.ibatis.executor.resultset.DefaultResultSetHandler@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {ErrorContext.instance().activity("handling results").object(mappedStatement.getId());final List<Object> multipleResults = new ArrayList<>();int resultSetCount = 0;// 获取第一个结果集,并将其包装成ResultSetWrapperResultSetWrapper rsw = getFirstResultSet(stmt);// 获取本次查询对应的ResultMap对象List<ResultMap> resultMaps = mappedStatement.getResultMaps();int resultMapCount = resultMaps.size();validateResultMapsCount(rsw, resultMapCount);while (rsw != null && resultMapCount > resultSetCount) {ResultMap resultMap = resultMaps.get(resultSetCount);// 真正处理结果集handleResultSet(rsw, resultMap, multipleResults, null);// 获取下一个结果集rsw = getNextResultSet(stmt);cleanUpAfterHandlingResultSet();resultSetCount++;}// ......
}

由 源码8 可知,在DefaultResultSetHandler对象的handleResultSets()方法中,结果集会被包装成ResultSetWrapper对象,真正处理结果集的方法是handleResultSet(),其参数包括ResultSetWrapper对象,以及从MappedStatement中取出来的ResultMap对象。

源码9org.apache.ibatis.executor.resultset.DefaultResultSetHandlerprivate void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults,ResultMapping parentMapping) throws SQLException {try {if (parentMapping != null) {// 当配置了<select>标签的resultSets属性时,parentMapping的值不为nullhandleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);} else if (resultHandler == null) {// 当没有指定ResultHandler时,创建默认的DefaultResultHandlerDefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);multipleResults.add(defaultResultHandler.getResultList());} else {// 最终都会调用handleRowValues()方法handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);}} finally {closeResultSet(rsw.getResultSet());}
}

由 源码9 可知,在DefaultResultSetHandler对象的handleResultSet()方法中,针对不同的情况做了一些逻辑判断,但最终都会调用handleRowValues()方法进行处理。

源码10org.apache.ibatis.executor.resultset.DefaultResultSetHandlerpublic void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {// 判断是否有嵌套ResultMapif (resultMap.hasNestedResultMaps()) {ensureNoRowBounds();checkResultHandler();handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);} else {handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);}
}

由 源码10 可知,在DefaultResultSetHandler对象的handleRowValues()方法中,会判断是否有嵌套ResultMap。如果有,则调用handleRowValuesForNestedResultMap()方法进行处理,否则调用handleRowValuesForSimpleResultMap()方法进行处理。

10.2.3.1 handleRowValuesForSimpleResultMap()

在【10.2.2 ResultMap解析过程】中指出,当使用<association>或<collection>标签,并以JOIN子句的方式配置一对一或一对多级联映射时,<association>或<collection>标签就相当于一个嵌套的ResultMap,此时hasNestedResultMaps属性为true。当使用外部Mapper的方式时,hasNestedResultMaps属性为false。

下面分析一下hasNestedResultMaps属性为false时的情况:

源码11org.apache.ibatis.executor.resultset.DefaultResultSetHandlerprivate void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {DefaultResultContext<Object> resultContext = new DefaultResultContext<>();ResultSet resultSet = rsw.getResultSet();skipRows(resultSet, rowBounds);// 遍历结果集对象,处理每一行数据while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);// 将结果集中的一行数据转换为Java实体对象Object rowValue = getRowValue(rsw, discriminatedResultMap, null);storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}
}

由 源码11 可知,在DefaultResultSetHandler对象的handleRowValuesForSimpleResultMap()方法中,会遍历结果集对象,处理每一行数据,调用getRowValue()方法将结果集中的每一行数据转换为Java实体对象。

源码12org.apache.ibatis.executor.resultset.DefaultResultSetHandlerprivate Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {final ResultLoaderMap lazyLoader = new ResultLoaderMap();// (1)创建结果对象Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final MetaObject metaObject = configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;if (shouldApplyAutomaticMappings(resultMap, false)) {// (2)处理自动映射,对未通过<result>等标签配置的映射进行处理foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}// (3)处理<result>等标签配置的映射信息foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;foundValues = lazyLoader.size() > 0 || foundValues;rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;}return rowValue;
}

由 源码12 可知,在DefaultResultSetHandler对象的getRowValue()方法做了三件事情:

(1)getRowValue()方法的第一步,调用createResultObject()方法创建结果对象。

源码13org.apache.ibatis.executor.resultset.DefaultResultSetHandlerprivate Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes,List<Object> constructorArgs, String columnPrefix) throws SQLException {// ......if (!constructorMappings.isEmpty()) {// 根据<constructor>标签配置的构造器映射找到构造方法return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs,columnPrefix);} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {// 使用默认构造方法return objectFactory.create(resultType);} else if (shouldApplyAutomaticMappings(resultMap, false)) {// 使用标注了@AutomapConstructor注解的构造方法return createByConstructorSignature(rsw, resultMap, columnPrefix, resultType, constructorArgTypes,constructorArgs);}// throw ...
}

由 源码13 可知,createResultObject()方法会在按照【<constructor>标签配置的构造器映射→默认构造方法→标注了@AutomapConstructor注解的构造方法】的顺序找到结果对象的构造方法,通过构造方法创建一个结果对象。

(2)getRowValue()方法的第二步,调用applyAutomaticMappings()方法处理自动映射,对未通过<result>等标签配置的数据库字段与Java实体属性的映射进行处理。

源码14org.apache.ibatis.executor.resultset.DefaultResultSetHandlerprivate boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,String columnPrefix) throws SQLException {// 获取需要进行自动映射的UnMappedColumnAutoMapping对象// UnMappedColumnAutoMapping封装了数据库字段与Java实体属性的关联关系List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);boolean foundValues = false;if (!autoMapping.isEmpty()) {// 遍历for (UnMappedColumnAutoMapping mapping : autoMapping) {// 获取数据库记录中该字段的值final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);if (value != null) {foundValues = true;}if (value != null || configuration.isCallSettersOnNulls() && !mapping.primitive) {// 调用实体对象对应的MetaObject对象的setValue()方法进行赋值metaObject.setValue(mapping.property, value);}}}return foundValues;
}

由 源码14 可知,applyAutomaticMappings()方法首先会找到需要进行自动映射的UnMappedColumnAutoMapping对象集合,UnMappedColumnAutoMapping对象封装了没有通过<result>等标签配置的数据库字段与Java实体属性的对应关系。

接着对UnMappedColumnAutoMapping对象集合进行遍历,获取数据库字段的值,并调用Java实体对象对应的MetaObject对象的setValue()方法进行赋值。

借助Dubug工具,可以查看测试案例执行到这一步的结果:

由图可知,createAutomaticMappings()找到了2个需要进行自动映射的属性,恰好就是测试案例中注释掉的age属性和phone属性。整个方法执行完后,可以发现User实体的age属性和phone属性已被成功赋值。

(3)getRowValue()方法的第三步,调用applyPropertyMappings()方法处理通过<result>等标签配置的数据库字段与Java实体属性的映射。

源码15org.apache.ibatis.executor.resultset.DefaultResultSetHandlerprivate boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {final Set<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);boolean foundValues = false;// 获取通过标签配置的映射集合final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();// 遍历映射集合for (ResultMapping propertyMapping : propertyMappings) {// 获取数据库字段名(包含前缀处理)String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);if (propertyMapping.getNestedResultMapId() != null) {column = null;}if (propertyMapping.isCompositeResult()|| column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))|| propertyMapping.getResultSet() != null) {// 获取数据库字段对应的值Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader,columnPrefix);// 获取Java实体属性的名称final String property = propertyMapping.getProperty();if (property == null) {continue;}if (value == DEFERRED) {foundValues = true;continue;}if (value != null) {foundValues = true;}if (value != null|| configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()) {// 为Java实体对象的属性赋值metaObject.setValue(property, value);}}}return foundValues;
}

由 源码15 可知,applyPropertyMappings()方法的逻辑很清晰,即获取所有通过标签指定的映射信息并遍历,然后找到数据库字段对应的值,最后为Java实体赋值。

借助Dubug工具,可以查看测试案例执行时获取的映射集合:

由图可知,映射集合共有4个ResultMapping对象,分别对应Mapper配置文件中配置的userId、name、birthday、orderList属性。要注意的是,orderList属性是一个<collection>标签,它的select属性被记录在ResultMapping对象的nestedQueryId属性上。

测试案例执行时applyPropertyMappings()方法的执行结果:

由图可知,applyPropertyMappings()方法执行完后,用户信息及其关联的订单信息均被查询出来了。

实际上,也是在applyPropertyMappings()方法中,<collection>标签中配置的外部Mapper被执行了。

(4)深入applyPropertyMappings()方法中的getPropertyMappingValue()方法,执行外部Mapper。

源码16org.apache.ibatis.executor.resultset.DefaultResultSetHandlerprivate Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {// 如果ResultMapping对象的nestedQueryId属性不为空,则进行嵌套查询if (propertyMapping.getNestedQueryId() != null) {return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);}// 正常获取数据库字段值的逻辑if (propertyMapping.getResultSet() != null) {addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?return DEFERRED;} else {final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);return typeHandler.getResult(rs, column);}
}

由 源码16 可知,getPropertyMappingValue()方法会判断ResultMapping对象的nestedQueryId属性是否不为空,如果不为空则进行嵌套查询,否则执行正常获取数据库字段值的逻辑。

在测试案例中,orderList属性是一个<collection>标签,它的select属性被记录在ResultMapping对象的nestedQueryId属性上,因此在获取该属性对应的值时,会进行嵌套查询。

源码17org.apache.ibatis.executor.resultset.DefaultResultSetHandlerprivate Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {// 获取外部Mapper的IDfinal String nestedQueryId = propertyMapping.getNestedQueryId();final String property = propertyMapping.getProperty();// 根据Mapper的ID获取对应的MappedStatement对象,并准备参数final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping,nestedQueryParameterType, columnPrefix);Object value = null;if (nestedQueryParameterObject != null) {// ......} else {final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,nestedQueryParameterObject, targetType, key, nestedBoundSql);if (propertyMapping.isLazy()) {lazyLoader.addLoader(property, metaResultObject, resultLoader);value = DEFERRED;} else {// 获取结果value = resultLoader.loadResult();}}}return value;
}

由 源码17 可知,getNestedQueryMappingValue()方法会根据外部Mapper的ID从Configuration对象中获取对应的MappedStatement对象;然后利用MappedStatement对象创建一个ResultLoader对象,在调用ResultLoader对象的loadResult()方法获取结果。

源码18org.apache.ibatis.executor.loader.ResultLoaderpublic Object loadResult() throws SQLException {List<Object> list = selectList();resultObject = resultExtractor.extractObjectFromList(list, targetType);return resultObject;
}private <E> List<E> selectList() throws SQLException {Executor localExecutor = executor;if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {localExecutor = newExecutor();}try {// 调用Executor对象的query()方法执行查询操作return localExecutor.query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER,cacheKey, boundSql);} // finally ......
}

由 源码18 可知,ResultLoader对象的loadResult()方法会转调selectList()方法,而该方法会调用Executor对象的query()方法执行查询操作。

在测试案例中,通过这一次额外查询,orderList属性的值也被填充好了:

至此,getRowValue()方法(源码12)全部执行完毕,handleRowValuesForSimpleResultMap()方法(源码11)得到了最终的查询结果:

10.2.3.2 handleRowValuesForNestedResultMap()

下面再来看看 源码10 中,hasNestedResultMaps属性为true时的情况,即存在嵌套ResultMap,此时会调用handleRowValuesForNestedResultMap()方法。

这个方法与上面分析的逻辑不同的地方在于:applyPropertyMappings()方法中不会对orderList属性进行赋值,而是在这后面加了一个applyNestedResultMappings()方法来赋值。

具体源码不再展开,分析方法和上面的分析逻辑类似。

······

本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/774122.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

卸载cuda

cd /usr/local/cuda-xx.x/bin/ sudo ./cuda-uninstaller sudo rm -rf /usr/local/cuda-xx.x 不行就 彻底清理CUDA安装(多版本一起清除)_cuda卸载干净-CSDN博客 还得 sudo apt-get --purge remove "cuda*" sudo apt-get --purge remove "*nvidia*"

为什么Solana在区块链生态系统中脱颖而出

当我们在不断发展的区块链技术世界中航行时&#xff0c;认识到平台不仅要跟上创新的步伐&#xff0c;还要突破可能的界限&#xff0c;这一点至关重要。#Solana 已成为领先的竞争者&#xff0c;这就是为什么这个高性能区块链的未来看起来很光明。 &#x1f31f; 可扩展性和速度&…

二分(蓝桥备赛)

1、借教室&#xff08;NOIP2012提高组&#xff09; 问题描述 在大学期间&#xff0c;经常需要租借教室。 大到院系举办活动&#xff0c;小到学习小组自习讨论&#xff0c;都需要向学校申请借教室。 教室的大小功能不同&#xff0c;借教室人的身份不同&#xff0c;借教室的手续…

什么是函数指针?如何定义和使用函数指针?

什么是函数指针&#xff1f;如何定义和使用函数指针&#xff1f; 函数指针是指向函数的指针&#xff0c;它存储了函数的地址&#xff0c;通过这个地址&#xff0c;程序可以间接地调用并执行这个函数。函数指针在C语言中常用于实现回调函数、函数表等高级功能&#xff0c;提高了…

单臂路由和三层交换机

目录 一.单臂路由 1.单臂路由的工作原理 2.单臂路由的配置 2.1画出拓扑图 2.2配置PC 2.3配置交换机 2.4配置路由器 2.5测试 二.三层交换机 1.三层交换机的概述 2.三层交换机的配置 2.1画出拓扑图 2.2配置PC 2.3配置二层交换机 2.4配置三层交换机 2.5测试 3.拓展 三.总结 一.…

Trello国内替代工具有哪些?分享5款

盘点5款类似Trello的本地部署项目管理工具&#xff1a;1.PingCode&#xff1b;2.Worktile&#xff1b;3.Teambition&#xff1b;4.redmine&#xff1b;5.TAIga.io。 Trello是一款杰出的协作与工作管理应用&#xff0c;专为追踪团队项目、凸显当前活动任务、分配责任人&#xff…

web全栈架构师第16期教程

教程介绍 互联网时代已进入后半场&#xff0c;行业环境发生了显著变化。互联网人&#xff0c;尤其是技术人员&#xff0c;如何在加速更迭的技术浪潮中持续充电&#xff0c;提升自身价值&#xff0c;是当下必须面对的挑战。课程涉及了现下前端实际开发时所需要的各块内容&#…

编程语言|C语言——C语言基本数据类型

前言 针对不同的数据&#xff0c;采取不同的存储方式和进行不同的处理。随着处理对象的复杂化&#xff0c;数据类型也要变得更丰富。数据类型的丰富程度直接反映了程序设计语言处理数据的能力。 C语言很重要的一个特点是它的数据类型十分丰富。因此&#xff0c;C语言程序数据处…

求解vue3警告

Invalid prop: type check failed for prop “activeType”. Expected TheTypedFn, got Number with value 0. 警告 复现问题 从demo.vue跳转到after-sale/index.vue页面 //demo.vue <div v-for"(obj, index) in list" :key"index" style"margi…

【Altium】ADTOP层器件如何变更到Bottom层及层颜

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 AD22 PCB TOP层器件变更到Bottom层及Bottom颜色的修改 2、 问题场景 在PCB布局评估设计时&#xff0c;考虑到器件布局摆放的合理性&#xff0c;有些器件需要布局放置在Bottom层&#xff0c;另外根据个人的设计风格习…

【hexo博客6】自定义域名 购买、配置、更新部署

【hexo博客6】自定义域名 购买、配置、更新部署 写在最前面自定义域名购买域名DNS配置Github 配置 更新部署博客 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#…

TS改变类型的方法

在 TypeScript 中&#xff0c;变量的类型一旦声明后是不可变的&#xff0c;即无法直接修改已经声明的变量的类型。这种设计是为了提高代码的清晰性和可维护性。如果需要将一个变量从一个类型转换为另一个类型&#xff0c;可以借助类型断言、类型转换函数或者新建一个变量来存储…

【图像处理】-1.图像二值化

1. 阈值概念 阈值&#xff1a;根据这个值将整张图像分割成不同的前后背景&#xff0c;在二值化处理中&#xff0c;有固定阈值和自适应阈值两种形式&#xff1b; 两种阈值使用情况区分&#xff1a; 当图像质量好&#xff0c;且目标和背景容易区分时&#xff0c;可以使用固定阈值…

ardupilot开发 --- 机载(边缘)计算机-VISP-附录 篇

我要这铁棒有何用 1. 源码附录1.1 tutorial-grabber-opencv-hyl.cpp 1. 源码附录 1.1 tutorial-grabber-opencv-hyl.cpp /*! \example tutorial-grabber-opencv.cpp */ #include <stdlib.h> #include <visp3/core/vpImageConvert.h> #include <visp3/gui/vpDi…

MySQL的安装(Linux版)

1.所需要的文件 MySQL.zip 2. 卸载自带的Mysql-libs # 查看是否存在 rpm -qa | grep mariadb# 如果存在则执行命令进行卸载 rpm -e --nodeps mariadb-libs3.在/opt目录下创建MySQL目录并上传所需要的安装包 cd /optmkdir MySQL4.按照编号顺序安装&#xff08;压缩包在解压完…

springboot多模块

一、demo 1、创建父项目 首先使用 Spring Initializr 来快速创建好一个Maven工程。然后删除无关的文件&#xff0c;只需保留pom.xml 文件。 &#xff08;1&#xff09;new Project -> spring initializr快速构建SpringBoot&#xff0c;artifactId为springbootmodules&…

nginx 正向代理 https

问题背景 因为网络环境受限&#xff0c;应用服务器无法直接访问外网&#xff0c;需要前置机上中转一下&#xff0c;这种情况可在应用服务器修改/etc/hosts文件指向前置机&#xff0c;在前置机上的nginx设置四层代理&#xff0c;从而出站。 方案 根据How to Use NGINX as an …

手把手教你 - JMeter压力测试

前言 压力测试是每一个Web应用程序上线之前都需要做的一个测试&#xff0c;他可以帮助我们发现系统中的瓶颈问题&#xff0c;减少发布到生产环境后出问题的几率&#xff1b;预估系统的承载能力&#xff0c;使我们能根据其做出一些应对措施。所以压力测试是一个非常重要的步骤&…

文案转化率低?快看看这两个坑你踩没踩

对于很多中小企业来说&#xff0c;无论是来拓市场还是获客&#xff0c;软文营销都会成为他们的主要营销方式&#xff0c;比较软文营销的成本较低&#xff0c;同时门槛也不高。但是也有品牌发现&#xff0c;自己和团队辛苦构思了几周写出的文案&#xff0c;但是效果不是很好。今…

数学分析复习:一元函数的极限

文章目录 函数极限定义和性质函数极限的另一种定义Cauchy收敛准则 本篇文章适合个人复习翻阅&#xff0c;不建议新手入门使用 函数极限 定义和性质 定义&#xff1a;函数极限 设函数 f ( x ) f(x) f(x) 若 ∀ ε > 0 , ∃ δ > 0 , ∀ x ∈ O o ( x 0 , δ ) , s . t …