7. MyBatis 的 ResultSetHandler(一)

MyBatis 的 ResultSetHandler

StatementHandler参数处理&结果处理,分别委托给ParameterHandler&ResultSetHandler(注意不是ResultHandler)。上一篇文章介绍了ParameterHandler,本文介绍ResultSetHandler

一、ResultSetHandler接口

我们先看下ResultSetHandler接口定义:

java 复制代码
public interface ResultSetHandler {
  // 处理结果集,返回结果列表
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  // 处理游标结果集,返回Cursor对象(用于流式处理)
  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  // 处理存储过程的输出参数
  void handleOutputParameters(CallableStatement cs) throws SQLException;
}

可以看到,ResultSetHandler接口定义了三种不同场景下,处理JDBC结果集的方法。其核心职责是将数据库返回的ResultSet转换为Java对象

  • handleResultSets:核心方法,处理JDBC的Statement的结果集,注意普通查询和存储过程查询,都使用这个方法的结果集。
  • handleCursorResultSets:处理Mapper的返回值是Cursor的方法。
  • handleOutputParameters:处理存储过程的输出参数,存储过程会使用handleResultSets处理结果集,使用本方法,处理输出类型的参数。

二、DefaultResultSetHandler 的核心实现

MyBatis仅提供一个默认实现类DefaultResultSetHandler,我们这里分析下核心的handleResultSets方法源码。

1. 整体流程:handleResultSets 方法

handleResultSets是处理查询结果集的核心方法,负责把Statement的结果集转换为Java对象,需要注意,无论是普通的查询还是存储过程查询,都是调用这个方法。handleOutputParameters只是处理存储过程的参数的(处理参数类型为OUT/INOUT类型的)。

注意这个方法名字,handleResultSets,有个s,暗示了结果集可能是多个

java 复制代码
    @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;

      // 获取第一个结果集
      ResultSetWrapper rsw = getFirstResultSet(stmt);
      // 获取MappedStatement中定义的结果集映射(可能有多个,对应多结果集)
      List<ResultMap> resultMaps = mappedStatement.getResultMaps();
      int resultMapCount = resultMaps.size();
      validateResultMapsCount(rsw, resultMapCount);

      // 第一部分:处理 MappedStatement 中定义的 resultMaps 对应的结果集
      while (rsw != null && resultSetCount < resultMaps.size()) {
        ResultMap resultMap = resultMaps.get(resultSetCount);
        // 处理当前结果集,映射为Java对象
        handleResultSet(rsw, resultMap, multipleResults, null);
        // 获取下一个结果集(用于存储过程多结果集场景)
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }

      // 第二部分:处理MappedStatement中resultSets指定的附加结果集(用于解决N+1)
      String[] resultSets = mappedStatement.getResultSets();
      if (resultSets != null) {
        while (rsw != null && resultSetCount < resultSets.length) {
          ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
          // 解决N+1,向父对象设置属性。
          if (parentMapping != null) {
            String nestedResultMapId = parentMapping.getNestedResultMapId();
            ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
            handleResultSet(rsw, resultMap, multipleResults, parentMapping);
          }
          rsw = getNextResultSet(stmt);
          cleanUpAfterHandlingResultSet();
          resultSetCount++;
        }
      }

      // 合并结果(如果只有一个结果集,直接返回该列表)
      return collapseSingleResultList(multipleResults);
    }

核心流程分为两部分

  1. 处理MappedStatement中定义的 resultMaps 对应的结果集。
  2. 处理MappedStatement中resultSets指定的附加结果集,用于解决N+1问题。

两部分都可能是多个!!!

原来,Mybatis可以支持一次获取多个结果集,很神奇。那么如何返回多个结果集呢?

  1. # Mybatis 如何返回多个结果集
  2. 也可以通过存储过程,这个官网有例子XML 映射器-结果映射(搜索ResultSets,可以解决N+1问题,这是第二个循环的来源)。

2. 单结果集处理:handleResultSet 方法

handleResultSet 负责处理单个结果集,根据是否有自定义 ResultHandler 选择不同的处理逻辑:

java 复制代码
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
  try {
    if (parentMapping != null) {
      // 处理嵌套结果(就是上个步骤中的第二个循环)
      handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
    } else {
      if (resultHandler == null) {
        // 无自定义ResultHandler时,使用默认的DefaultResultHandler收集结果
        DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
        handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
        multipleResults.add(defaultResultHandler.getResultList());
      } else {
        // 有自定义ResultHandler时,使用其处理结果
        handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
      }
    }
  } finally {
    // 关闭结果集
    closeResultSet(rsw.getResultSet());
  }
}

3. 行记录映射:handleRowValues 与 getRowValue 方法

无论有无自定义ResultHandler,都是调用handleRowValues方法:

java 复制代码
    // 处理结果集中的行记录
    public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
      if (resultMap.hasNestedResultMaps()) {
        // 处理嵌套结果映射(如包含association/collection)
        ensureNoRowBounds();
        checkResultHandler();
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
      } else {
        // 处理普通结果映射(无嵌套)
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
      }
    }

看下处理普通映射的方法(处理嵌套的有点复杂,平时不怎么用,看起来头晕):

java 复制代码
    // 处理普通结果映射(逐行映射)
    private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
      DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
      // 跳过RowBounds指定的偏移量,内存分页
      skipRows(rsw.getResultSet(), rowBounds);
      // 循环处理每行记录,直到达到RowBounds的限制或结果集结束
      while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
        // 解析鉴别器(discriminator),动态选择ResultMap
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
        // 将当前行映射为Java对象
        Object rowValue = getRowValue(rsw, discriminatedResultMap);
        // 存储对象(调用ResultHandler处理)
        storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
      }
    }

Q:discriminator是什么鬼东西??

A:动态识别返回对象类型 ,比如我们定义了一个动物类,查询结果需要动态转为阿猫阿狗,就可以使用这个特性,可以在官网中查看使用方法:XML 映射器-结果映射(搜索discriminator)。

接下来看下核心的getRowValue方法。

java 复制代码
    // 将单行记录映射为Java对象
    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
      final ResultLoaderMap lazyLoader = new ResultLoaderMap();
      // 创建结果对象(通过ObjectFactory)
      Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
      if (rowValue != null && !typeHandlerRegistry.hasTypeHandler(rowValue.getClass())) {
        // 获取结果对象的元信息(用于反射设置属性)
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        // 处理结果映射(ResultMapping),设置对象属性
        if (shouldApplyAutomaticMappings(resultMap, false)) {
          // 自动映射(未在ResultMap中显式配置的字段)
          foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
        }
        // 显式映射(ResultMap中配置的result/association/collection)
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
      }
      return rowValue;
    }

getRowValue 方法的核心逻辑:

  1. 创建结果对象 :通过 ObjectFactory 实例化目标对象(如 User)。
  2. 自动映射 :对 ResultMap 中未显式配置的字段,根据列名与属性名的匹配关系自动映射。
  3. 显式映射 :根据 ResultMap 中配置的 resultassociationcollection 等标签,通过 TypeHandlerResultSet 中的列值转换为对象属性。

4. applyAutomaticMappings

除了使用ResultMap标签进行映射,Mybatis还支持自动映射,也就是通过蛇形转驼峰的方式,将数据库字段,映射成Java属性:

java 复制代码
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, 
                                     MetaObject metaObject, String columnPrefix) throws SQLException {
  // 1. 创建自动映射列表:筛选出未显式配置但存在匹配的字段
  List<UnMappedColumnAutoMapping> autoMappings = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
  if (autoMappings.isEmpty()) {
    return false;
  }

  boolean foundValues = false;
  // 2. 遍历自动映射列表,逐个处理字段
  for (UnMappedColumnAutoMapping mapping : autoMappings) {
    // 2.1 通过 TypeHandler 从结果集获取字段值(并转换为 Java 类型)
    final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
    if (value != null) {
      foundValues = true;
    }
    // 2.2 通过 MetaObject 给对象属性赋值(支持 null 值处理配置)
    if (value != null || configuration.isCallSettersOnNulls() && !mapping.primitive) {
      // 给属性赋值(如 user.setName(value))
      metaObject.setValue(mapping.property, value);
    }
  }
  return foundValues;
}
关键步骤解析
  1. 创建自动映射列表(createAutomaticMappings :获取未映射的字段,与JavaBean的属性建立映射关系,并根据JDBC的类型,绑定TypeHandler,带有缓存,防止每次都重复映射。
  2. 通过TypeHandler转换类型 :熟悉的TypeHandler,与上一节相反,这个是从JDBC转为Java类型。
  3. 通过MetaObject赋值 :利用 MyBatis 的反射工具 MetaObject 给目标对象的属性赋值,支持配置 callSettersOnNulls(即使值为null也调用 setter 方法),以及对基本类型(primitive)的特殊处理(避免给基本类型赋null导致异常)。

applyPropertyMappings方法,为Java对象赋值的逻辑与这个相同,但还有些处理嵌套和延迟加载的逻辑,这里不深入研究了。

三、小结

ResultSetHandler是负责将JDBC 结果集(ResultSet)转换为Java对象的核心组件。本文对他的实现做了浅显的分析,复杂的嵌套结果映射,以及嵌套结果的延迟加载,本文没有涉及(有点复杂)。

到此为止,MyBatis四大核心组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)已经分析完成,后续准备分析下Mybatis的插件机制,这个可能是我们了解Mybatis源码最重要的用途。

相关推荐
用户4822137167756 分钟前
C++——纯虚函数、抽象类
后端
张同学的IT技术日记16 分钟前
必看!用示例代码学 C++ 基础入门,快速掌握基础知识,高效提升编程能力
后端
UserNamezhangxi24 分钟前
kotlin 协程笔记
java·笔记·kotlin·协程
林太白25 分钟前
Nuxt3 功能篇
前端·javascript·后端
咖啡里的茶i34 分钟前
数字化图书管理系统设计实践(java)
java·课程设计
得物技术1 小时前
营销会场预览直通车实践|得物技术
后端·架构·测试
九转苍翎1 小时前
Java内功修炼(2)——线程安全三剑客:synchronized、volatile与wait/notify
java·thread
曲莫终1 小时前
正则表达式删除注释和多余换航
java·kotlin
Ice__Cai1 小时前
Flask 入门详解:从零开始构建 Web 应用
后端·python·flask·数据类型
武子康1 小时前
大数据-74 Kafka 核心机制揭秘:副本同步、控制器选举与可靠性保障
大数据·后端·kafka