Mybatis 源码 ④ :TypeHandler

文章目录

  • 一、前言
  • 二、DefaultParameterHandler
    • 1. DefaultParameterHandler#setParameters
      • 1.1 UnknownTypeHandler
      • 1.2 自定义 TypeHandler
  • 三、DefaultResultSetHandler
    • 1. hasNestedResultMaps
    • 2. handleRowValuesForNestedResultMap
      • 2.1 resolveDiscriminatedResultMap
      • 2.2 createRowKey
      • 2.3 getRowValue
        • 2.2.1 createResultObject
        • 2.2.2 applyAutomaticMappings
        • 2.2.3 applyPropertyMappings
        • 2.2.4 applyNestedResultMappings
      • 2.4 storeObject
    • 3. handleRowValuesForSimpleResultMap

一、前言

Mybatis 官网 以及 本系列文章地址:

  1. Mybatis 源码 ① :开篇
  2. Mybatis 源码 ② :流程分析
  3. Mybatis 源码 ③ :SqlSession
  4. Mybatis 源码 ④ :TypeHandler
  5. Mybatis 源码 ∞ :杂七杂八

书接上文 Mybatis 源码 ③ :SqlSession
。我们这里来看下 DefaultParameterHandler 和 DefaultResultSetHandler 的处理过程。

二、DefaultParameterHandler

DefaultParameterHandler 类图如下,可以看到其实现了 ParameterHandler 接口,我们可以通过 Plugin 的方式对 ParameterHandler 进行增强。这里我们主要来看 DefaultParameterHandler 的具体作用。
在这里插入图片描述

1. DefaultParameterHandler#setParameters

在 SimpleExecutor 和 BaseExecutor doUpdate、doQuery、doQueryCursor 等方法中会调用 prepareStatement 方法,在其中会调用 StatementHandler#parameterize 来对参数做预处理,里面会调用 PreparedStatementHandler#parameterize,该方法如下:

  @Overridepublic void parameterize(Statement statement) throws SQLException {// 这里会调用 DefaultParameterHandler#setParametersparameterHandler.setParameters((PreparedStatement) statement);}

因此我们可以知道,在Sql 执行前,会调用 DefaultParameterHandler#setParameters 方法来对参数做处理,这也就给了 TypeHandler 的参数转换提供了条件。


DefaultParameterHandler#setParameters 实现如下:

  @Overridepublic void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());// 获取当前Sql执行时的参数List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();if (parameterMappings != null) {for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();// 对一些额外参数处理if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional paramsvalue = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 判断是否有合适的类型转换器,可以解析当前参数// 这里个人理解是为了判断参数是否是单独参数,value = parameterObject;} else {// 根据 参数名去获取参数传入的值。MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 如果当前参数指定了类型转换器, 则通过类型转换器进行转换。否则交由 UnknownTypeHandler TypeHandler typeHandler = parameterMapping.getTypeHandler();JdbcType jdbcType = parameterMapping.getJdbcType();if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();}try {// 调用类型转换器进行处理, 默认情况下是 UnknownTypeHandler // jdbcType 是我们通过 jdbcType 属性指定的类型,没有指定则为空 typeHandler.setParameter(ps, i + 1, value, jdbcType);} catch (TypeException | SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}}

上面可以看到逻辑比较简单:遍历所有参数,并且参数值交由 typeHandler.setParameter 来处理。需要注意的是这里的 typeHandler 如果没有指定默认是 UnknownTypeHandler。在UnknownTypeHandler 中则会根据参数实际类型来从注册的 TypeHandler 中选择合适的处理器来处理。下面我们具体来看。

1.1 UnknownTypeHandler

UnknownTypeHandler#setParameter 会调用 UnknownTypeHandler#setNonNullParameter, 我们以该方法为例,UnknownTypeHandler 的其他方法也类似。

  @Overridepublic void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)throws SQLException {// 根据参数类型来获取 类型处理器// jdbcType 是我们通过 jdbcType 属性指定的类型,没有指定则为空 TypeHandler handler = resolveTypeHandler(parameter, jdbcType);// 调用类型处理器处理handler.setParameter(ps, i, parameter, jdbcType);}private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {TypeHandler<?> handler;// 参数为空直接返回 ObjectTypeHandlerif (parameter == null) {handler = OBJECT_TYPE_HANDLER;} else {// 从注册的 TypeHandler 中根据类型选择合适的处理器handler = typeHandlerRegistrySupplier.get().getTypeHandler(parameter.getClass(), jdbcType);// check if handler is null (issue #270)// 如果没找到返回 ObjectTypeHandlerif (handler == null || handler instanceof UnknownTypeHandler) {handler = OBJECT_TYPE_HANDLER;}}return handler;}

这里可以看到, 在执行Sql前会通过 DefaultParameterHandler#setParameters 对参数做一次处理。

  1. 如果参数指定了 typeHandler 则使用参数指定的 TypeHandler
  2. 如果参数没有指定,则使用 UnknownTypeHandler 来处理。而 UnknownTypeHandler 会根据参数的实际类型和 jdbcType 来从已注册的 TypeHandler 选择合适的处理器对参数做处理。

1.2 自定义 TypeHandler

我们可以自定义 TypeHandler 来实现指定字段的特殊处理,如用户密码在数据库中不能明文展示,而在代码中我们明文处理,则就可以通过如下方式定义:

  1. 创建一个 PwdTypeHandler 类,继承 BaseTypeHandler
public class PwdTypeHandler extends BaseTypeHandler<String> {// 定义加解密方式private static final SymmetricCrypto AES = new SymmetricCrypto(SymmetricAlgorithm.AES, "1234567890123456".getBytes());// 赋值时加密@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setString(i, AES.encryptBase64(parameter));}// 取值时解密@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException {return AES.decryptStr(rs.getString(columnName));}@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return AES.decryptStr(rs.getString(columnIndex));}@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return AES.decryptStr(cs.getString(columnIndex));}
}
  1. XML 指定使用的 typeHandler,如下
    <resultMap id="BaseResultMap" type="com.kingfish.entity.SysUser"><!--@Table sys_user--><result property="id" column="id" jdbcType="INTEGER"/><result property="userName" column="user_name" jdbcType="VARCHAR"/><result property="password" column="password" jdbcType="VARCHAR" typeHandler="com.kingfish.config.handler.PwdTypeHandler"/><!-- 忽略其他字段 --></resultMap>
  1. 在实际调用接口时新增或返回时都会使用 PwdTypeHandler 来对指定字段做处理人,如下:
    1. 调用接口明文新增时入库是加密后结果 在这里插入图片描述
    2. 数据库加密,查询返回是明文
      在这里插入图片描述

三、DefaultResultSetHandler

DefaultResultSetHandler实现了ResultSetHandler 接口,ResultSetHandler 见名知意,即为结果集合处理器。所以下面我们来看看该方法的具体逻辑 :

  @Overridepublic List<Object> handleResultSets(Statement stmt) throws SQLException {ErrorContext.instance().activity("handling results").object(mappedStatement.getId());final List<Object> multipleResults = new ArrayList<>();int resultSetCount = 0;// 获取第一个结果集  ResultSet 并包装成 ResultSetWrapper ResultSetWrapper rsw = getFirstResultSet(stmt);List<ResultMap> resultMaps = mappedStatement.getResultMaps();// ResultMap 的数量, 当使用存储过程时,可能会有多个,我们这里不考虑存储过程的多个场景。int resultMapCount = resultMaps.size();// ResultMap 数量校验 :rsw != null && resultMapCount < 1validateResultMapsCount(rsw, resultMapCount);/**********************************************************************/// 1.对 ResultMap 的处理// 循环所有的 ResultMapwhile (rsw != null && resultMapCount > resultSetCount) {// 获取当前 ResultMapResultMap resultMap = resultMaps.get(resultSetCount);// 1.1 根据ResultMap中定义的映射规则处理ResultSet,并将映射得到的Java对象添加到 multipleResults集合中保存handleResultSet(rsw, resultMap, multipleResults, null);// 1.2 获取下一个 ResultSet rsw = getNextResultSet(stmt);// 1.3 清理nestedResultObjects集合,这个集合是用来存储中间数据的cleanUpAfterHandlingResultSet();resultSetCount++;}/**********************************************************************/// 2. 对 ResultSets 的处理// 对 resultSet 处理,<select>标签可以通过 resultSets 属性指定String[] resultSets = mappedStatement.getResultSets();if (resultSets != null) {// 处理reusltSet while (rsw != null && resultSetCount < resultSets.length) {// 获取ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);if (parentMapping != null) {String nestedResultMapId = parentMapping.getNestedResultMapId();ResultMap resultMap = configuration.getResultMap(nestedResultMapId);handleResultSet(rsw, resultMap, null, parentMapping);}// 获取下一个 ResultSetrsw = getNextResultSet(stmt);// 清理nestedResultObjects集合,这个集合是用来存储中间数据的cleanUpAfterHandlingResultSet();resultSetCount++;}}/**********************************************************************/// 返回结果集return collapseSingleResultList(multipleResults);}

这里可以看到, DefaultResultSetHandler#handleResultSet 方法的逻辑分为对 ResultMap 的处理和 对 ResultSets 的处理,在涉及存储过程的情况下会返回 ResultSets ,该部分不在本文的讨论范围内,在 Mybatis 官方文档 中对该属性做了具体的描述 : 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。具体使用如下图:
在这里插入图片描述
业务使用方面可以详参: https://blog.csdn.net/qq_40233503/article/details/94436578


本文主要看对 ResultMap 的处理内容,而其中最主要的则是 DefaultResultSetHandler#handleResultSet 方法,具体实现如下:

 private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {try {// 父级 mapper 不为空的情况 :在处理 ResultSet 时会出现,不在本文讨论范围if (parentMapping != null) {handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);} else {// 1. 未指定 ResultHandler 情况 : 如果 resultHandler  为空则创建一个 DefaultResultHandler 作为默认处理器// 这里的 resultHandler 是我们调用 Mapper Interface Method 方法时指定的。如果没指定则为空if (resultHandler == null) {// 如果没指定则使用默认的 DefaultResultHandler 来处理结果DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);multipleResults.add(defaultResultHandler.getResultList());} else {// 2. 指定了 ResultHandler 情况 : 将 resultHandler 传入作为结果处理器handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);}}} finally {// issue #228 (close resultsets)closeResultSet(rsw.getResultSet());}}

上面可以看到这里针对了 未指定 ResultHandler 情况 和 指定了 ResultHandler 情况做了判断:我们可以在 Mapper Interface Method 入参中传入 ResultHandler 来对返回结果集做处理。(也可传入 RowBounds 对返回结果集做逻辑分页,但是需要注意 RowBounds 仅是逻辑分页,数据已经查出,所以不建议使用),通过实现ResultHandler 接口来对该查询的结果进行定制化解析(需要注意方法不能有返回值,因为返回值已经交由 resultHandler 来处理了),当 Mybatis 将结果查询出后会交由 resultHandler#handleResult 方法来处理。在方法入参中传入 ResultHandler 实例,并且返回值为 void,如下指定了 selectByParam 方法查询的结果交由 ResultHandler 来处理:

    void selectByParam(ResultHandler resultHandler);

而实际上无论 ResultHandler 指定与否,都会调用 DefaultResultSetHandler#handleRowValues 方法来解析行数据,所以我们来看看该方法的具体实现,如下:

  // 处理行数据 : 该方法会获取并解析出来每一行的数据public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {//  1. 如果有嵌套的 ResultMap,即 ResultMap#hasNestedResultMaps = trueif (resultMap.hasNestedResultMaps()) {// 嵌套前的判断1 :嵌套情况下,如果 safeRowBoundsEnabled 为true,则不能使用 RowBounds (确切的说只能使用 默认的 RowBounds )// safeRowBoundsEnabled 可以通过 {mybatis.configuration.safe-row-bounds-enabled} 配置,代表 允许在嵌套语句中使用分页(RowBounds)	, 默认 trueensureNoRowBounds();// 嵌套前的判断2 :嵌套情况下,如果 safeResultHandlerEnabled 为 true && 语句属性 resultOrdered 为 true 则抛出异常// safeResultHandlerEnabled 可以通过 {mybatis.configuration.safe-result-handler-enabled} 配置,代表 允许在嵌套语句中使用分页(ResultHandler)。默认 truecheckResultHandler();// 2. 处理嵌套 ResultMaphandleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);} else {// 3. 无嵌套 ResultMap的 简单逻辑handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);}}

这里可以看到,对于行数据的处理分为嵌套情况和非嵌套情况,如下 :

  1. DefaultResultSetHandler#hasNestedResultMaps :通过 ResultMap#hasNestedResultMaps 属性判断当前是否是嵌套结果集,成立条件是 <resultMap> 标签中使用了子标签 <association><collection> ,并且标签没有指定 select 属性 或使用了 <case> 标签。(如果指定了select属性,则会保存在 ResultMapping#nestedQueryId 指定 查询id)。 该属性在于 ResultMap.Builder#build 中会初始化,如下:
    在这里插入图片描述

  2. DefaultResultSetHandler#handleRowValuesForNestedResultMap :用来处理嵌套结果集的情况,即如果上面的判断成立了,则执行该逻辑。

  3. DefaultResultSetHandler#handleRowValuesForSimpleResultMap :用来处理简单查询的情况,无嵌结果集的情况。


下面我们详细来看上面的详细逻辑

1. hasNestedResultMaps

DefaultResultSetHandler#hasNestedResultMaps 方法的作用是判断当前 ResultMap 是否是嵌套结果集,其判断依据是 ResultMap#hasNestedResultMaps = true,如下:

  public boolean hasNestedResultMaps() {return hasNestedResultMaps;}

而 ResultMap#hasNestedResultMaps 属性的初始化是在ResultMap.Builder#build 中完成,如下:

在这里插入图片描述

这里我们关注两个属性:ResultMap#hasNestedQueries (标记当前 ResultMap 是否有嵌套映射,判断依据是 ResultMapping#nestedQueryId != null)和 ResultMap#hasNestedResultMaps (标记当前 ResultMap 是否有嵌套结果集,判断依据是 ResultMapping#nestedResultMapId != null || ResultMapping#resultSet != null


我们以 XML 解析为例,在 XMLMapperBuilder#buildResultMappingFromContext中,会通过如下逻辑来解析取 nestedSelect、nestedResultMap 属性 :

在这里插入图片描述
并且在 MapperBuilderAssistant#buildResultMapping 方法中根据 nestedSelect、nestedResultMap 来给 ResultMapping#nestedQueryId 和 ResultMapping#nestedResultMapId 赋值,如下:
在这里插入图片描述


综上,这里的嵌套判断成立的条件是 :<resultMap> 标签中使用了子标签 <association><collection> ,并且标签没有指定 select 属性 或使用了 <case> 标签。(如果指定了select属性,则会保存在 ResultMapping#nestedQueryId 指定 查询id)。下面我们来简单介绍下这两种情况的区别。


对于嵌套映射,其存在两种实现方式:

  • 内部嵌套 : 使用 association、collection 标签但是不指定 select 属性,或使用case 标签。这种是通过一条 Sql 语句查询后关联处理。 下面DefaultResultSetHandler#handleRowValuesForNestedResultMap 的方法就是处理该情况
  • 外部嵌套 : 使用 association、collection 标签并指定 select 属性。这种是通过一条Sql语句执行后再根据select指定语句关联查询。下面DefaultResultSetHandler#applyPropertyMappings 中会对这种嵌套查询做处理

我们以如下两个表为例:

CREATE TABLE `sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',`user_name` varchar(255) DEFAULT NULL COMMENT '用户名',`password` varchar(255) DEFAULT NULL COMMENT '密码',`role_id` bigint(20) DEFAULT NULL COMMENT '角色id',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;CREATE TABLE `sys_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',`role_name` varchar(255) DEFAULT NULL COMMENT '用户名',`status` varchar(255) DEFAULT NULL COMMENT '状态',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
  1. 内部嵌套实现如下:

        <resultMap id="UserBaseResultMap" type="com.kingfish.entity.SysUser"><result property="id" column="id" jdbcType="INTEGER"/><result property="userName" column="user_name" jdbcType="VARCHAR"/><result property="password" column="password" jdbcType="VARCHAR" /><!-- 忽略余下属性 --></resultMap><resultMap id="BaseResultMap" type="com.kingfish.entity.SysRole"><!--@Table sys_role--><result property="id" column="id" jdbcType="INTEGER"/><result property="roleName" column="role_name" jdbcType="VARCHAR"/><result property="status" column="status" jdbcType="VARCHAR"/><!-- 忽略余下属性 --></resultMap><!-- 内部嵌套映射 --><resultMap id="InnerNestMap" type="com.kingfish.entity.dto.SysRoleDto" extends="BaseResultMap"><!-- 指定 sysUsers 属性都是前缀为 user_ 的属性 --><collection property="sysUsers" columnPrefix="user_"resultMap="UserBaseResultMap"></collection></resultMap><!-- 通过联表查询出来多个属性,如果属性名跟 sysUsers 对应的com.kingfish.dao.SysUserDao.BaseResultMap配置的属性名一致则会映射上去 (属性名映射规则受到columnPrefix影响) --><select id="selectRoleUser" resultMap="InnerNestMap">select sr.*, su.id user_id, su.user_name user_user_name, su.password user_passwordfrom sys_role srleft join sys_user su on sr.id = su.role_id</select>
    
  2. 外部嵌套实现如下:

    	<resultMap id="UserBaseResultMap" type="com.kingfish.entity.SysUser"><result property="id" column="id" jdbcType="INTEGER"/><result property="userName" column="user_name" jdbcType="VARCHAR"/><result property="password" column="password" jdbcType="VARCHAR" /><!-- 忽略余下属性 --></resultMap><!-- 外部嵌套映射 --><resultMap id="OutNestMap" type="com.kingfish.entity.dto.SysRoleDto" extends="BaseResultMap"><!-- 指定使用selectUser 作为 sysUsers 属性的查询语句 --><collection property="sysUsers" ofType="com.kingfish.entity.dto.SysUserDto"select="selectUser" column="{roleId=id}" ></collection></resultMap><select id="selectUser" resultMap="UserBaseResultMap">selectid,  user_name, passwordfrom sys_userwhere role_id = #{roleId}</select><select id="selectRole" resultMap="OutNestMap">select *from sys_role</select>

上述两种查询的返回结果都相同,如下:
在这里插入图片描述

关于该部分内容本文只做简单介绍,如有需要可详参:https://www.cnblogs.com/sanzao/p/11466496.html#_label1


2. handleRowValuesForNestedResultMap

上面我们介绍了嵌套条件成立的条件,当满足了上述条件后,说明了当前查询存在嵌套结果集,则调用 DefaultResultSetHandler#handleRowValuesForNestedResultMap 来处理,具体如下

  private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();ResultSet resultSet = rsw.getResultSet();// 跳过执行行数据,由 RowBounds.offset 属性决定skipRows(resultSet, rowBounds);Object rowValue = previousRowValue;// 确定当前剩余数据满足条件,即此次拉取的数据量 < RowBounds.limmit  时 且 连接未关闭 且后续还有结果集,则再次获取while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {// 1. 解析 discriminator 属性,获取 discriminator 指定的 ResultMap final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);// 2. 创建当前行记录的 缓存 keyfinal CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);// 尝试获取该行记录的缓存Object partialObject = nestedResultObjects.get(rowKey);// issue #577 && #542// resultOrdered = true 时if (mappedStatement.isResultOrdered()) {// 如果未缓存安全数据if (partialObject == null && rowValue != null) {// 清空缓存nestedResultObjects.clear();// 存储数据storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}// 3. 获取行数据 rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);} else {// 3. 获取行数据rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);// 4. 存储数据 : partialObject == null 说明数据没有被缓存if (partialObject == null) {storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}}}// 行数据不为空 && resultOrdered = true && 还需要查询更多行if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {// 存储数据storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);previousRowValue = null;} else if (rowValue != null) {previousRowValue = rowValue;}}

这里我们可以看到 :

  1. 利用 RowBounds 是可以实现分页的功能的,但却是一个逻辑分页,因为所有数据都是已经加载到内存后再根据 RowBounds 的分页限制选择是否丢弃或继续获取,因此并不建议使用。
  2. resolveDiscriminatedResultMap 方法实现了对 <discriminator > 标签的解析,并将 <discriminator >解析后的ResultMap 作为最终的 ResultMap 处理。下面我们会详细讲。
  3. getRowValue 方法会根据 resultMap 解析并获取当前的行数据。下面我们会详细讲。
  4. storeObject 方法会将处理后的行结果缓存起来。下面我们会详细讲。

2.1 resolveDiscriminatedResultMap

该方法的作用是为了解析 <discriminator> 标签, 内容比较简单,这里不在过多赘述。关于 <discriminator> 标签的用法,如有需要详参 Mybatis 源码 ∞ :杂七杂八

  public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {Set<String> pastDiscriminators = new HashSet<>();Discriminator discriminator = resultMap.getDiscriminator();while (discriminator != null) {// 获取 discriminator 指定的 column 的值final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);// 根据 column 的值来判断执行哪个 case 分支 : 根据 value 获取到  discriminatedMapId ,如果获取到则说明有对应的 case 分支final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));// 如果存在该 ResultMap if (configuration.hasResultMap(discriminatedMapId)) {// 用 discriminator 指定 ResultMap 替换现有的 resultMap resultMap = configuration.getResultMap(discriminatedMapId);Discriminator lastDiscriminator = discriminator;discriminator = resultMap.getDiscriminator();if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {break;}} else {break;}}return resultMap;}

2.2 createRowKey

createRowKey 方法 作用是创建当前行的缓存Key。具体实现如下:

  // 生成行数据的缓存Key,这里会将列名和列值都作为关键值创建Key// 在嵌套映射中会作为唯一标志标识一个结果对象private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException {final CacheKey cacheKey = new CacheKey();// 使用映射结果集的id 作为 CacheKey 的一部分cacheKey.update(resultMap.getId());// 获取 <result> 标签结果集List<ResultMapping> resultMappings = getResultMappingsForRowKey(resultMap);// 为空则判断返回类型是是不是Mapif (resultMappings.isEmpty()) {if (Map.class.isAssignableFrom(resultMap.getType())) {// 由结果集中的所有列名以及当前记录行的所有列值一起构成CacheKeycreateRowKeyForMap(rsw, cacheKey);} else {// 由结果集中未映射的列名以及它们在当前记录行中的对应列值一起构成CacheKey对象createRowKeyForUnmappedProperties(resultMap, rsw, cacheKey, columnPrefix);}} else {//  由ResultMapping集合中的列名以及它们在当前记录行中相应的列值一起构成CacheKeycreateRowKeyForMappedProperties(resultMap, rsw, cacheKey, resultMappings, columnPrefix);}// 如果除了映射结果集的id 之外没有任何属性参与生成CacheKey 则返回NULL_CACHE_KEYif (cacheKey.getUpdateCount() < 2) {return CacheKey.NULL_CACHE_KEY;}return cacheKey;}

这里我们不再具体分析具体的代码内容,直接总结具体的逻辑(下面内容来源 Mybatis源码阅读(三):结果集映射3.2 —— 嵌套映射):

  1. 尝试使用节点或者节点中定义的列名以及该列在当前记录行中对应的列值生成CacheKey
  2. 如果ResultMap中没有定义这两个节点,则有ResultMap中明确要映射的列名以及它们在当前记录行中对应的列值一起构成CacheKey对象
  3. 经过上面两个步骤后如果依然查不到相关的列名和列值,且ResultMap的type属性明确指明了结果对象为Map类型,则有结果集中所有列名以及改行记录行的所有列值一起构成CacheKey
  4. 如果映射的结果对象不是Map,则由结果集中未映射的列名以及它们在当前记录行中的对应列值一起构成CacheKey

额外需要注意的是 ,CacheKey 创建后,会尝试从 nestedResultObjects 中获取对象对数据。如下:

      Object partialObject = nestedResultObjects.get(rowKey);

nestedResultObjects 的作用是缓存所有查询出的结果数据,但是这里会存在问题:在嵌套映射时如果存在两行完全一样的数据,则会被忽略。该问题我们在 Mybatis 源码 ∞ :杂七杂八 进行了详细说明

2.3 getRowValue

getRowValue 方法是处理每一行的值,需要注意的是这里的 handleRowValuesForNestedResultMap 中调用的 getRowValue 方法和 handleRowValuesForSimpleResultMap 中调用的 getRowValue 方法是重载方法。


下面我们来具体看 handleRowValuesForNestedResultMap 中调用的 getRowValue 如下:

 // DefaultResultSetHandler#getRowValue(ResultSetWrapper, ResultMap, .CacheKey, String, Object)// 将数据库查出来的数据转换为  Mapper Interface Method 返回的类型private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {// 获取 ResultMap 的唯一IDfinal String resultMapId = resultMap.getId();// 外层数据赋值给 rowValueObject rowValue = partialObject;// 如果缓存有值,则认为是嵌套映射if (rowValue != null) {// 用外层数据生成元数据 metaObject final MetaObject metaObject = configuration.newMetaObject(rowValue);// 外层数据保存到 ancestorObjects 中putAncestor(rowValue, resultMapId);// 处理嵌套逻辑applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);// 从 ancestorObjects 中移除该数据ancestorObjects.remove(resultMapId);} else {final ResultLoaderMap lazyLoader = new ResultLoaderMap();//  1. 反射 Mapper Interface Method 返回的类型对象,这里尚未填充行数据rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);// rowValue 不为空 && 没有针对 rowValue 类型的 TypeHandler if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final MetaObject metaObject = configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;// 如果允许自动映射(可通过 <resultMap> 标签的 autoMapping 属性指定)if (shouldApplyAutomaticMappings(resultMap, true)) {// 2. 根据自动映射规则尝试映射,看是行数据是否能映射到对应的属性 (忽略大小写的映射)foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}// 3. 根据属性映射foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;putAncestor(rowValue, resultMapId);// 4. 对嵌套结果集进行映射foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;ancestorObjects.remove(resultMapId);foundValues = lazyLoader.size() > 0 || foundValues;// 如果 映射到了属性值 或者 配置了空数据返回实体类 (mybatis.configuration.return-instance-for-empty-row 属性指定)则 返回 rowValue, 否则返回空 rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;}// 缓存外层对象if (combinedKey != CacheKey.NULL_CACHE_KEY) {nestedResultObjects.put(combinedKey, rowValue);}}return rowValue;}

上面我们我们主要看下面几个方法:

  1. createResultObject : 这里会创建Mapper Interface Method 返回的类型对象,但是并没有对各个属性赋值。不过需要注意 createResultObject 方法创建返回对象时分为下面集中情况:

    1. 如果Mybatis 中注册了针对 ResultMap.type 类型的 TypeHandler,则会调用 TypeHandler#getResult 来获取结果
    2. 如果当前 ResultMap 指定了构造函数参数,则使用指定入参构造结果
    3. 如果 ResultMap.type 是接口类型或者 ResultMap.type 有默认构造函数,则通过 ObjectFactory#create 创建构造函数
    4. 如果开启了自动映射则按构造函数签名创建
    5. 如果上述情况都没匹配,则抛出异常。
  2. applyAutomaticMappings :如果开启了自动映射则会按照自动映射的规则(忽略属性大小写差异)进行属性映射

  3. applyPropertyMappings :根据规则对剩余属性进行映射

  4. applyNestedResultMappings : 处理嵌套映射的属性。


下面我们详细来看上面的几个方法的具体实现:

2.2.1 createResultObject

createResultObject 实现如下:

  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {this.useConstructorMappings = false; // reset previous mapping resultfinal List<Class<?>> constructorArgTypes = new ArrayList<>();final List<Object> constructorArgs = new ArrayList<>();// 根据 ResultMap 的属性通过反射方式创建一个对象(如果通过 <constructor>指定了构造参数 则注入构造参数)Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);// 对象不为空且没有对应的类型处理器if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();// 遍历所有属性for (ResultMapping propertyMapping : propertyMappings) {// issue gcode #109 && issue #149// 如果是嵌套结果集 && 并且开启了懒加载,则这里创建一个代理对象,等实际调用时才会触发获取逻辑if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);break;}}}// 标注是否使用了构造映射this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping resultreturn resultObject;}

2.2.2 applyAutomaticMappings

如果开启了自动映射则会按照自动映射的规则(忽略属性大小写差异)进行属性映射

  private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {// 获取开启自动映射的结果集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)) {// gcode issue #377, call setter on nulls (value is not 'found')metaObject.setValue(mapping.property, value);}}}return foundValues;}

2.2.3 applyPropertyMappings

这里是对剩下的属性进行映射,在上面我们提到过嵌套映射存在内部嵌套和外部嵌套两种情况。这里则会对外部嵌套的情况做处理。具体如下:

  private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)throws SQLException {// 获取使用 columnPrefix拼接后的列名final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);boolean foundValues = false;// 获取 ResultMap 的 reuslt 属性final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();// 遍历所有属性for (ResultMapping propertyMapping : propertyMappings) {// 获取拼接 columnPrefix 后 属性列名String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);// 如果当前属性存在嵌套的 ResultMap 则忽略该列,交由下面进行嵌套解析if (propertyMapping.getNestedResultMapId() != null) {// the user added a column attribute to a nested result map, ignore itcolumn = null;}// 如果当前查询有复合结果(嵌套映射时,可能出现一对一、一对多的情况) || 当前列匹配(property 与 column经过转换后一致)	|| 当前属性指定了 ResultSetif (propertyMapping.isCompositeResult()|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))|| propertyMapping.getResultSet() != null) {// 解析并获取属性对应的列值Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);// issue #541 make property optionalfinal String property = propertyMapping.getProperty();if (property == null) {continue;} else if (value == DEFERRED) {foundValues = true;continue;}if (value != null) {foundValues = true;}// value 不为空  || (配置 {mybatis.configuration.call-setters-on-nulls} 为 true && set 方法不为私有) if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {// gcode issue #377, call setter on nulls (value is not 'found')// 设置属性值metaObject.setValue(property, value);}}}return foundValues;}

可以看到上面的关键方法在于下面 getPropertyMappingValue 方法,具体实现如下:

  // 获取属性映射的值private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)throws SQLException {// 如果当前是嵌套属性if (propertyMapping.getNestedQueryId() != null) {// 获取嵌套属性查询的结果return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);} else if (propertyMapping.getResultSet() != null) {// 添加挂起的子关系addPendingChildRelation(rs, metaResultObject, propertyMapping);   // TODO is that OK?// 返回一个固定对象return DEFERRED;} else {// 最基础的解析使用指定的 TypeHandler 解析数据并返回。如 Long 使用 LongTypeHandler等final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();// 拼接前缀:即 prefix + columnNamefinal String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);// 获取处理结果并返回return typeHandler.getResult(rs, column);}}

上面我们可以看到,这里分成三种情况

  1. 外部嵌套:交由 getNestedQueryMappingValue 方法来处理
  2. 指定 ResultSet : 挂起子关系,等后续一起处理(不在本文分析内容)
  3. 最基础的解析:交由 TypeHandler 来获取结果集并返回对象

下面我们来看看 getNestedQueryMappingValue 嵌套解析的过程:

  // 获取嵌套查询的结果集映射private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)throws SQLException {// 获取嵌套映射id 即 select属性指定的查询语句final String nestedQueryId = propertyMapping.getNestedQueryId();final String property = propertyMapping.getProperty();// 获取嵌套映射指定的语句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) {final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);final Class<?> targetType = propertyMapping.getJavaType();// 如果结果已经被缓存if (executor.isCached(nestedQuery, key)) {// 延迟加载executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);value = DEFERRED;} else {final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);// 如果是懒加载则加载到 lazyLoader中并返回推迟加载对象if (propertyMapping.isLazy()) {lazyLoader.addLoader(property, metaResultObject, resultLoader);value = DEFERRED;} else {// 加载结果value = resultLoader.loadResult();}}}return value;}

2.2.4 applyNestedResultMappings

applyNestedResultMappings 则是针对内部嵌套进行处理,如下:

  private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {boolean foundValues = false;for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {final String nestedResultMapId = resultMapping.getNestedResultMapId();if (nestedResultMapId != null && resultMapping.getResultSet() == null) {try {// 获取拼接 parentPrefix 后的列名 :我们可以通过 <collection> <association> 的 columnPrefix 属性指定前缀,这里会进行前缀拼接final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);// 1. 获取嵌套映射对应的结果集final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);// 如果列前缀为空:一般情况下如果使用嵌套映射则会声明前缀if (resultMapping.getColumnPrefix() == null) {// try to fill circular reference only when columnPrefix// is not specified for the nested result map (issue #215)// 尝试获取祖先对象Object ancestorObject = ancestorObjects.get(nestedResultMapId);if (ancestorObject != null) {// 如果是新对象,则进行链接 : 当第一次处理当前嵌套映射时认为是新对象,可以简单认为没有放入 nestedResultObjects 缓存if (newObject) {// 链接对象linkObjects(metaObject, resultMapping, ancestorObject); // issue #385}continue;}}// 创建行的keyfinal CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);// 与父级 key 进行组合:final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);// 从缓存中获取该行对象Object rowValue = nestedResultObjects.get(combinedKey);boolean knownValue = rowValue != null;// 如果对象是集合类型,则判断是否需要初始化,需要则创建爱你instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory// 据notNullColumn属性, 检测是否有非空属性,如果全为空则没必要解析if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {// 获取映射结果rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);// 如果映射结果不为空 && 不是缓存对象 则链接对象// 这里的判断会引发一个问题 : 在嵌套映射时如果两个对象完全一致会被缓存命中从而不会链接对象,导致数据丢失,下面会讲if (rowValue != null && !knownValue) {linkObjects(metaObject, resultMapping, rowValue);foundValues = true;}}} catch (SQLException e) {throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'.  Cause: " + e, e);}}}return foundValues;}// 链接对象private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {// 必要的话初始化集合对象final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);// 如果集合对象不为空,则添加到集合对象中if (collectionProperty != null) {final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);targetMetaObject.add(rowValue);} else {// 否则的话保存属性到元数据中metaObject.setValue(resultMapping.getProperty(), rowValue);}}

这里需要注意的是由于 Mybatis 的 RowKey 是属性名 + 属性值拼接,在嵌套时如果两行数据完全一致,则第一行数据会被缓存,当处理第二行数据时,会被缓存命中从而不满足 rowValue != null && !knownValue 的判断条件,导致数据丢失。

2.4 storeObject

storeObject 方法将数据保存起来, 具体实现如下:

  private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {// 如果父 ResultMap 存在 (嵌套模式),则链接到 父 ResultMap  中 if (parentMapping != null) {linkToParents(rs, parentMapping, rowValue);} else {// 回调 resultHandler 来处理结果callResultHandler(resultHandler, resultContext, rowValue);}}private void linkToParents(ResultSet rs, ResultMapping parentMapping, Object rowValue) throws SQLException {// 获取到父ResultMapping  中该属性的缓存keyCacheKey parentKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(), parentMapping.getForeignColumn());// 获取缓存的对象List<PendingRelation> parents = pendingRelations.get(parentKey);if (parents != null) {for (PendingRelation parent : parents) {if (parent != null && rowValue != null) {// 将当前对象注入到父级linkObjects(parent.metaObject, parent.propertyMapping, rowValue);}}}}private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {resultContext.nextResultObject(rowValue);// 调用ResultHandler#handleResult来处理结果,默认情况是 DefaultResultHandler,将结果保存到 DefaultResultHandler#list 中((ResultHandler<Object>) resultHandler).handleResult(resultContext);}

3. handleRowValuesForSimpleResultMap

该方法用来解析非嵌套映射情况,具体实现如下:

  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)throws SQLException {DefaultResultContext<Object> resultContext = new DefaultResultContext<>();ResultSet resultSet = rsw.getResultSet();// 跳过执行行数据,由 RowBounds.offset 属性决定skipRows(resultSet, rowBounds);// 确定当前剩余数据满足条件,即此次拉取的数据量 < RowBounds.limmit  时 且 连接未关闭 且后续还有结果集,则再次获取while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {// 1. 解析 discriminator 属性ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);// 2. 获取行数据 Object rowValue = getRowValue(rsw, discriminatedResultMap, null);// 3. 保存映射后的数据storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}}// 跳过指定的行数private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {rs.absolute(rowBounds.getOffset());}} else {for (int i = 0; i < rowBounds.getOffset(); i++) {if (!rs.next()) {break;}}}}  // 是否应该获取更多列private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();}

这里我们可以看到 :

  1. 利用 RowBounds 是可以实现分页的功能的,但却是一个逻辑分页,因为所有数据都是已经加载到内存后再根据 RowBounds 的分页限制选择是否丢弃或继续获取,因此并不建议使用。

  2. resolveDiscriminatedResultMap 方法实现了对 <discriminator > 标签的解析,并将 <discriminator >解析后的ResultMap 作为最终的 ResultMap 处理,上面已经介绍,不再赘述。

  3. getRowValue 方法会根据 resultMap 解析并获取当前的行数据, 这个跟上面不同是个重载方法,如下:

     // 将数据库查出来的数据转换为  Mapper Interface Method 返回的类型private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {final ResultLoaderMap lazyLoader = new ResultLoaderMap();//  1. 反射 Mapper Interface Method 返回的类型对象,这里尚未填充行数据Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);// rowValue 不为空 && 没有针对 rowValue 类型的 TypeHandler if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final MetaObject metaObject = configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;// 如果允许自动映射(可通过 <resultMap> 标签的 autoMapping 属性指定)if (shouldApplyAutomaticMappings(resultMap, false)) {// 2. 根据自动映射规则尝试映射,看是行数据是否能映射到对应的属性 (忽略大小写的映射)foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}// 3. 根据属性映射foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;foundValues = lazyLoader.size() > 0 || foundValues;// 如果 映射到了属性值 或者 配置了空数据返回实体类 (mybatis.configuration.return-instance-for-empty-row 属性指定)则 返回 rowValue, 否则返回空 rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;}// 返回映射后的实体类return rowValue;}
  4. storeObject 方法会将处理后的行结果缓存起来。上面已经介绍,这里不再赘述。

至此整个解析过程已经结束。


以上:内容部分参考
https://www.jianshu.com/p/cdb309e2a209
https://zhuanlan.zhihu.com/p/526147349
https://blog.csdn.net/qq_40233503/article/details/94436578
https://blog.csdn.net/weixin_42893085/article/details/105105958
https://blog.csdn.net/weixin_40240756/article/details/108889127
https://www.cnblogs.com/hongshaozi/p/14160328.html
https://www.jianshu.com/p/05f643f27246
https://www.cnblogs.com/sanzao/p/11466496.html
https://juejin.cn/post/6844904127823085581
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

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

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

相关文章

K8S系列二:实战入门

写在前面 本文是K8S系列第二篇&#xff0c;主要面向对K8S新手同学&#xff0c;阅读本文需要读者对K8S的基本概念&#xff0c;比如Pod、Deployment、Service、Namespace等基础概念有所了解。尚且不熟悉的同学推荐先阅读本系列的第一篇文章&#xff1a;《K8S系列一&#xff1a;概…

远程控制医疗行业应用解析:如何满足医院合规需求?

远程控制医疗行业应用解析&#xff1a;如何满足医院合规需求&#xff1f; 作为一个起源于IT行业的技术&#xff0c;以远程桌面为基础的远程控制技术目前在医疗领域也已经有了比较广阔的应用前景&#xff0c;尤其是在医疗数字化系统/设备的远程运维场景&#xff0c;已经有了一些…

如何正确下载tomcat???

亲爱的小伙伴&#xff0c;千万别再去找下网站下载啦&#xff0c;这样詪容易携带病毒。 我们去官方网址下载。 Apache Tomcat - Welcome! 最后下载解压即可。。。

2024软考系统架构设计师论文写作要点

一、写作注意事项 系统架构设计师的论文题目对于考生来说&#xff0c;是相对较难的题目。一方面&#xff0c;考生需要掌握论文题目中的系统架构设计的专业知识;另一方面&#xff0c;论文的撰写需要结合考生自身的项目经历。因此&#xff0c;如何将自己的项目经历和专业知识有机…

SQL server中substring 的用法

一&#xff1a;substring函数是SQL中截取字段数据中的其中一部分 --列&#xff1a;提取abdcsef中的abc数据&#xff0c;使用substring实现select substring(abdcsef,1,3) --‘1’表示截取的起始位置是从第一个字符开始,‘3’表示截取后得到的字符串长度为3个字符 二&#xff1…

React源码解析18(7)------ 实现事件机制(onClick事件)

摘要 在上一篇中&#xff0c;我们实现了useState的hook&#xff0c;但由于没有实现事件机制&#xff0c;所以我们只能将setState挂载在window上。 而这一篇主要就是来实现事件系统&#xff0c;从而实现通过点击事件进行setState。 而在React中&#xff0c;虽然我们是将事件绑…

前后端分离------后端创建笔记(07)表单验证

1、我输入数据&#xff0c;然后关闭&#xff0c;重新打开会发现残存的数据仍然保留着 2、点了这个x号&#xff0c;数据就全部被清理了 3、点这三个地方&#xff0c;数据全部都清理掉 4、这里先写一个方法 4.1 定义一个方法 4.2 这里表单的数据在哪里&#xff0c;就是这个 4.3 …

在 Linux 中使用 cp 命令

cp 命令是 Linux 中一个重要的命令&#xff0c;你可能经常会用到它。 正如名称所示&#xff0c;cp 代表 复制copy&#xff0c;它被用于 在 Linux 命令行中复制文件和目录。 这是一个相对简单的命令&#xff0c;只有几个选项&#xff0c;但你仍有必要深入了解它。 在展示 cp …

VLLM推理流程梳理

0x0. 前言 本文在对VLLM进行解析时只关注单卡情况&#xff0c;忽略基于ray做分布式推理的所有代码。 0x1. 运行流程梳理 先从使用VLLM调用opt-125M模型进行推理的脚本看起&#xff1a; from vllm import LLM, SamplingParams# Sample prompts. prompts ["Hello, my n…

二次封装element-plus上传组件,提供校验、回显等功能

二次封装element-plus上传组件 0 相关介绍1 效果展示2 组件主体3 视频组件4 Demo 0 相关介绍 基于element-plus框架&#xff0c;视频播放器使用西瓜视频播放器组件 相关能力 提供图片、音频、视频的预览功能提供是否为空、文件类型、文件大小、文件数量、图片宽高校验提供图片…

el-table实现懒加载(el-table-infinite-scroll)

2023.8.15今天我学习了用el-table对大量的数据进行懒加载。 效果如下&#xff1a; 1.首先安装&#xff1a; npm install --save el-table-infinite-scroll2 2.全局引入&#xff1a; import ElTableInfiniteScroll from "el-table-infinite-scroll";// 懒加载 V…

clion2020.3配置clang-format

标题clion 启用clang-format 文件->设置->编辑器->代码样式. 为了保持原有代码风格不变&#xff0c;可以把原始的配置风格先导出&#xff0c;最好直接保存到自己的工程下&#xff0c;.clang-format是隐藏文件&#xff0c;需要用ctrlH才能看到 文件->设置->编辑…

SpringBoot复习:(45)@Component定义的bean会被@Bean定义的同名的bean覆盖

有同名的bean需要配置&#xff1a; spring.main.allow-bean-definition-overridingtrue 否则报错。 package cn.edu.tju.component;import org.springframework.stereotype.Component;Component public class Person {private String name;private int age;{this.name "…

OpenHarmony Meetup 广州站 OpenHarmony正当时—技术开源

招募令 OpenHarmony Meetup 广州站 火热招募中&#xff0c;等待激情四射的开发者&#xff0c;线下参与OpenHarmonyMeetup线下交流 展示前沿技术、探讨未来可能、让你了解更多专属OpenHarmony的魅力 线下参与&#xff0c;先到先得,仅限20个名额&#xff01; 报名截止时间8月23日…

【云原生】Docker 详解(三):Docker 镜像管理基础

Docker 详解&#xff08;三&#xff09;&#xff1a;Docker 镜像管理基础 1.镜像的概念 镜像可以理解为应用程序的集装箱&#xff0c;而 Docker 用来装卸集装箱。 Docker 镜像含有启动容器所需要的文件系统及其内容&#xff0c;因此&#xff0c;其用于创建并启动容器。 Dock…

Go学习-Day1

Go学习-Day1 个人博客&#xff1a;CSDN博客 打卡。 Go语言的核心开发团队&#xff1a; Ken Thompson (C语言&#xff0c;B语言&#xff0c;Unix的发明者&#xff0c;牛人)Rob Pike(UTF-8发明人)Robert Griesemer(协助HotSpot编译器&#xff0c;Js引擎V8) Go语言有静态语言的…

MongoDB安装

文章目录 MongoDB安装设置yum源安装指定版本的mongodb配置文件连接MongoDB的工具MongoDBCompass MongoDB安装 设置yum源 [rootWDQCVM sbin]# vim /etc/yum.repos.d/mongodb-org-6.0.repo [mongodb-org-6.0] nameMongoDB Repository baseurlhttps://repo.mongodb.org/yum/red…

JavaScript如何执行语句

目录 语法/词法分析 预编译 解释执行 预编译什么时候发生 js运行三步曲 预编译前奏 预编译步骤 巩固基础练习 语法/词法分析 按语句块的粒度解析成抽象语法树 ,分析该js脚本代码块的语法是否正确&#xff0c;如果出现不正确&#xff0c;则向外抛出一个语法错误&#x…

第4章:决策树

停止 当前分支样本均为同一类时&#xff0c;变成该类的叶子节点。当前分支类型不同&#xff0c;但是已经没有可以用来分裂的属性时&#xff0c;变成类别样本更多的那个类别的叶子节点。当前分支为空时&#xff0c;变成父节点类别最多的类的叶子节点。 ID3 C4.5 Cart 过拟合 缺…

文本挖掘 day5:文本挖掘与贝叶斯网络方法识别化学品安全风险因素

文本挖掘与贝叶斯网络方法识别化学品安全风险因素 1. Introduction现实意义理论意义提出方法&#xff0c;目标 2. 材料与方法2.1 数据集2.2 数据预处理2.3 关键字提取2.3.1 TF-IDF2.3.2 改进的BM25——BM25WBM25BM25W 2.3.3 关键词的产生(相关系数) 2.4 关联规则分析2.5 贝叶斯…