Mybatis流程分析(九): 讲透Mybatis结果集处理逻辑,设计具有通用性的结果集处理器

本系列文章皆在从细节 着手,由浅入深的分析Mybatis框架内部的处理逻辑,带你从一个全新的角度来认识Mybatis的工作原理。

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


前言

在之前Mybatis流程分析(八): 从JDBC出发,透彻分析Mybatis执行sql的原理中,我们从原生JDBC入手,分析了Mybaits内部执行sql语句的秘密。本质来看Mybatissql执行的底层逻辑就是将sql处理逻辑委托给statement对象,

进一步,当sql语句执行完毕后,下一步要做的就是对结果集进行处理。此时Mybatis内部又会将处理逻辑委托结果集处理器的ResultSetHandler来完成。

接下来,我们便来看看Mybatis内部在处理结果集时与我们之前JDBC操纵ResultSet有何区别。

JDBC操纵结果集

与之前分析一样,在分析Mybaits内部是如何来对结果集进行处理之前,我们先来看看原生jdbc中是如何来对结果集进行操纵的。

java 复制代码
public class JDBCDemo {
   
    // .... 省略驱动加载
    
    // 1. 建立数据库连接
   Connection connection = DriverManager.getConnection(url, username, password);
            
    // 2. 创建SQL语句
    Statement statement = connection.createStatement();
    
    // 3. 执行SQL查询
    String selectSql = "SELECT * FROM yourtable";
    ResultSet resultSet = statement.executeQuery(selectSql);

    //4. 处理查询结果
    while (resultSet.next()) {
        int id = resultSet.getInt("id");
        String name = resultSet.getString("name");
      // 处理封装数据
       }
    }
}

可以看到,上述代码的流程大致可以总结为如下的流程:

1. 创建并执行SQL查询

  • 使用 Connection 建立数据库连接。
  • 创建 StatementPreparedStatement 对象并执行 SQL 查询。
  • 获取 ResultSet 对象来存储查询结果。

2. 遍历和操作结果集

  • 使用 ResultSet 对象的 next() 方法遍历结果集中的每一行数据。
  • 使用 getXXX() 方法从结果集中获取特定列的值,其中 XXX 表示数据类型。

总之,操作 ResultSet 的主要步骤是创建查询、遍历结果集 。而在遍历结果集时,使用 getXXX() 方法获取列的值,其中 XXX 根据列的数据类型进行选择,然后对数据进行相应的操作。此外,确保在完成后关闭所有相关资源以避免资源泄漏。

此时,不妨考虑这样一个问题。即如何让操纵结果集的流程变得更加通用 。那何为通用呢?就是说只暴露给用户这样只有一个接口,其只需传入sql语句,并配置一个需要返回的bean对象信息,程序就能执行sql语句,并将sql执行结果封装为其配置的bean

如果你是框架的设计者对于这一需求,你该如何来进行程序设计呢?我想你大概率会采用一个Map结构来保存映射信息。这样,在循环处理结果集时,只需遍历对应Map就可以获取对应的名称,进而也就可以获取到相应的内容了。如果你这样去想,说明你对于Mybatis内部结果集的处理其实理解的已经理解的八九不离十啦~~~

Mybatis内部对于结果集的处理

接下来,不妨跟着笔者的思路,让我们来看看Mybatis内部究竟是如何来处理ResultSet进行处理的。

但在分析之前,笔者想问大家一个问题,对于结果集的处理流程我们该从如何看起呢?换言之,Mybatis内部对于结果集的处理从何处开始的呢?如果你能脱口回答出说出:当然是从 SimpleExecutorquery方法开始,进而进入到PreparedStatementHandler中的query方法啦!因为这是Mybatis内部就是通过这样的调用逻辑来执行sql的。

对于这样的答案,笔者将感到十分欣慰。这说明你曾认真读过笔者之前的文章,笔者真的很感谢的你的信任和认可,感谢你肯花出时间来阅读笔者的拙作🙆‍

接下来,就让我们看看PreparedStatementHandler中的query方法内部是如何对结果集进行处理的。

java 复制代码
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    String sql = boundSql.getSql();
    // 通过对象statemnt中的exectute方法来完成sql的执行
    statement.execute(sql);
    // 结果集处理
    return resultSetHandler.handleResultSets(statement);
  }

可以看到,在PreparedStatementHandler中的query方法内部其会将结果集的处理逻辑交给resultSetHandler来完成。

事实上,ResultSetHandlerMybatis内部的一个重要的组件,它负责将数据库查询的结果集(ResultSet)转换成Java对象或Java集合的操作。换言之,ResultSetHandler的主要任务是处理数据库查询结果的映射,将结果集中的数据转化为应用程序可以操作的Java对象。

此外,在大多数情况下Mybatis内部会自动选择合适的ResultSetHandler实现,但最常用的还是DefaultResultSetHandler

结果集处理逻辑

进一步,DefaultResultSetHandler中方法handleResultSet的逻辑如下:

DefaultResultSetHandler # handleResultSet()

java 复制代码
public List<Object> handleResultSets(Statement stmt) 
                        throws SQLException {
  
  // 持有最后封装的结果信息
  final List<Object> multipleResults = new ArrayList<>();

  // 省略其他无关代码.....
  
  // 获取MappedStatment中存储的ResultSetMap信息
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
 
 
  // 应对配合的ResultMap标签
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    // ..... 省略其他无关代码
  }
  
  // 主要处理标签中配置的ResultSet信息
  String[] resultSets = mappedStatement.getResultSets();
  if (resultSets != null) {
    while (rsw != null && resultSetCount < resultSets.length) {
        // .....省略其他无关代码
        handleResultSet(rsw, resultMap, null, parentMapping);
     
       // .....省略其他无关代码
    }
  }

  return collapseSingleResultList(multipleResults);
}

针对上述代码中出现的ResultSetResutMap,我们有必要介绍一些Mybatis中有关ResultSetResutMap的相关知识。

事实上,在Mybatis内部ResultSetResutMap都可用于配置返回内容,但两者也是有区别的。具体如下:

  1. ResultMapResultMap 用于定义单个查询结果的映射规则。你可以在 sql 映射文件中定义一个或多个 ResultMap,并将它们与不同的查询语句关联。例如配置如下的ResultMap映射信息

    xml 复制代码
    <resultMap id="userResultMap" type="User">
        <id property="id" column="user_id" />
        <result property="username" column="user_name" />
    </resultMap>

此时,你可以在查询语句中引用这些 ResultMap,告诉Mybatis如何将查询结果映射到 Java 对象。例如,可以通过如下的方式使用配置的ResultMap信息。

xml 复制代码
    <select id="selectUser" resultMap="userResultMap">
        SELECT user_id, user_name 
        FROM users 
        WHERE user_id = #{userId}
    </select>
  1. ResultSet : 通过可以在sql 映射文件的 <select> 元素中使用 resultSets 属性来指定一个或多个结果集的名称。

此时,你可以在代码中获取这些结果集的名称,然后根据名称来处理每个结果集的映射规则,这可能涉及到不同的 ResultMap。进一步,mappedStatement.getResultSets() 方法用于获取与 <select> 元素中的 resultSets 属性关联的结果集名称数组。因为resultSets 属性用于指定一个或多个嵌套查询的结果集的名称。例如,在以下的 sql 映射关系中:

xml 复制代码
    <select id="selectUsersWithOrders" 
            resultSets="org.example.User">
        SELECT user_id, user_name 
        FROM users 
        WHERE user_id = #{userId}
    </select>

mappedStatement.getResultSets() 将返回一个字符串数组 org.example.User,其中包含了 resultSets 属性中指定的结果集名称。

总之,在进行结果关系映射时,可以根据具体的需求选择使用 ResultMapmappedStatement.getResultSets(),甚至可以同时使用它们,以便更灵活地配置返回内容和结果集处理规则。

明白了,ResultSetResutMapMybatis中的作用后,让我们继续深入到DefaultResultSetHandler内部的handleResultSet方法看看其是如何来完成结果集处理的。

java 复制代码
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    
   // ... 省略其他无关方法
   
   if (parentMapping != null) {
      handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
   }
   // ... 省略其他无关方法
}

可以看到,在 handleResultSet内部,其又会将对于结果集的处理逻辑委托给方法handleRowValues来完成。事实上,这部分逻辑是相当绕的,在此我们就不一一赘述了。如下这张图的准确的反映了handleRowValues后续的调用逻辑。

sequenceDiagram SimplerExecutor ->> PreparedStatementHandler: query PreparedStatementHandler ->> DefaultResultSetHandler:handlerResutlSets par 结果集内部处理逻辑 DefaultResultSetHandler ->> DefaultResultSetHandler: handlerResultSet and DefaultResultSetHandler ->> DefaultResultSetHandler: handleRowValues and DefaultResultSetHandler ->> DefaultResultSetHandler: handleRowValuesForSimplkeResultMap and DefaultResultSetHandler ->> DefaultResultSetHandler: getRowValue alt getRow内部逻辑判断 else shouldApplyAutomaticMappings DefaultResultSetHandler ->> DefaultResultSetHandler : applyAutomaticMappings end DefaultResultSetHandler ->> DefaultResultSetHandler: applyPropertyMappings end

可以看到,在getRowValue方法内部,其会通过shouldApplyAutomaticMappings用以判断是否配置了相关的映射规则,如果配置了则执行applyAutomaticMappings返回配置类型对应的bean。进一步,applyAutomaticMappings内部的逻辑如下:

java 复制代码
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
 
 List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);

  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将内容信息对bean中对应属性信息进行设置
        metaObject.setValue(mapping.property, value);
      }
    }
  }
  return foundValues;
}

对于applyAutomaticMappings方法大致逻辑如下:

  1. 首先,在 final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);中,其首先通过 mapping.typeHandler 获取结果集中指定列 mapping.column 的值,这个值会被映射到 Java 对象属性。(注:这部分信息可在之前介绍过的resultMap中进行配置)

  2. 其次,在metaObject.setValue(mapping.property, value);中,其通过 metaObject(元对象)将结果集中的值 value 设置到 Java 对象的属性 mapping.property 中。这是实现自动映射的核心操作,它将数据库查询结果中的列映射到 Java 对象的属性上。

总结

本文,首先以原生JDBC对于结果集的处理为起始点,分析了原生JDBC对于结果集的处理操作细节。以此为基础,我们详细分析了MyBatis内部对于结果集处理的全流程。但无论如何你应该明白,在Mybatis内部,对于结果集的处理本质和原生JDBC操纵结果没什么明显区别。此外,为了增强结果集处理的通用性Mybatis内部会引入一个ResultMap结构来存储sqlJava实体实体对象间的映射关系,从而增强了框架处理结果集时的通用性。

至此,我们对于Mybatis内部的分析也就告一段落了,后续笔者会在写一文章将对前面内容进行一个总结和回顾,希望能帮助你更加成体系的了解Mybatis的处理逻辑。

相关推荐
F-2H1 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱05671 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
_oP_i2 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx2 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康2 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘3 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意3 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
刘大辉在路上4 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
FF在路上4 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进4 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html