MyBatis11-《通用源码指导书:MyBatis源码详解》笔记-executor包(下)

本系列文章是我从《通用源码指导书:MyBatis源码详解》一书中的笔记和总结
本书是基于MyBatis-3.5.2版本,书作者 易哥 链接里是CSDN中易哥的微博。但是翻看了所有文章里只有一篇简单的介绍这本书。并没有过多的展示该书的魅力。接下来我将自己的学习总结记录下来。如果作者认为我侵权请联系删除,再次感谢易哥提供学习素材。本段说明将伴随整个系列文章,尊重原创,本人已在微信读书购买改书。
版权声明:本文为CSDN博主「架构师易哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/onlinedct/article/details/107306041

1.语句处理功能

1.1.MyBatis对多语句类型的支持

在 MyBatis映射文件的编写中,我们常会见到“${}”和“#{}”这两种定义变量的符号,其含义如下。

  • ${}:使用这种符号的变量将会以字符串的形式直接插到 SQL片段中。
  • #{}:使用这种符号的变量将会以预编译的形式赋值到 SQL片段中。

MyBatis中支持三种语句类型,不同语句类型支持的变量符号不同。MyBatis中的三种语句类型如下。

  • STATEMENT:这种语句类型中,只会对 SQL片段进行简单的字符串拼接。因此,只支持使用“${}”定义变量。
  • PREPARED:这种语句类型中,会先对 SQL片段进行字符串拼接,然后对 SQL片段进行赋值。因此,支持使用“${}”“#{}”这两种形式定义变量。
  • CALLABLE:这种语句类型用来实现执行过程的调用,会先对 SQL 片段进行字符串拼接,然后对 SQL片段进行赋值。因此,支持使用“${}”“#{}”这两种形式定义变量。
<!--直接字符串拼接,必须自己加引号,否则会拼接为下面的语句然后失败::SELECT * FROM `user` WHERE schoolName = Sunny School-->
    <select id="queryUserBySchoolName_A" resultType="com.github.yeecode.mybatisdemo.model.User" statementType="STATEMENT">
      SELECT * FROM `user` WHERE schoolName = "${schoolName}"
    </select>

    <!--会把变量转为?后进行填入,不能有引号-->
    <select id="queryUserBySchoolName_B" resultType="com.github.yeecode.mybatisdemo.model.User" statementType="PREPARED">
      SELECT * FROM `user` WHERE schoolName = #{schoolName}
    </select>

    <!--使用前需要先到数据库创建以下存储过程:-->
    <!--CREATE PROCEDURE `yeecode`(IN `ageMinLimit` int,IN `ageMaxLimit` int,OUT `count` int, OUT `maxAge` int)-->
    <!--BEGIN-->
    <!--SELECT COUNT(*),MAX(age) INTO count,maxAge FROM user WHERE age >= ageMinLimit AND age <= ageMaxLimit;-->
    <!--END $$-->
    <select id="runCall" statementType="CALLABLE">
      CALL yeecode(
      ${ageMinLimit},
      #{ageMaxLimit,mode=IN,jdbcType=NUMERIC},
      #{count,mode=OUT,jdbcType=NUMERIC},
      #{maxAge,mode=OUT,jdbcType=NUMERIC}
      );
    </select>

因为 STATEMENT、PREPARED 形式的 SQL 语句比较常用,不再单独介绍。下面详细介绍 CALLABLE语句的使用。

在对CALLABLE语句进行调用时,可以直接使用Map来设置输入参数。使用MyBatis调用存储过程的操作如下代码所示。

 Map<String, Integer> param = new HashMap<>();
 param.put("ageMinLimit",10);
 param.put("ageMaxLimit",30);
 session.selectOne("com.github.yeecode.mybatisdemo.dao.UserMapper.runCall",param);
 System.out.println("proceduce param :" + param);

存储过程调用后,MyBatis会根据输出参数设置直接将输出结果写回给定的 Map参数中,键为变量名,值为存储过程结果。

1.2.MyBatis的语句处理功能

statement 子包负责提供语句处理功能,其中StatementHandler 是语句处理功能类的父接口。StatementHandler接口及其子类的类图如图:
在这里插入图片描述
其中 RoutingStatementHandler 类是一个代理类,它能够根据传入的 MappedStatement对象的具体类型选中一个具体的被代理对象,然后将所有实际操作都委托给被代理对象。功能如其名,RoutingStatementHandler 类提供的是路由功能,而路由选择的依据就是语句类型。

  // 根据语句类型选取出的被代理类的对象
  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    
    
    // 根据语句类型选择被代理对象
    switch (ms.getStatementType()) {
    
    
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }
  }

BaseStatementHandler 作为三个实现类的父类,提供了实现类的公共方法。并且BaseStatementHandler类使用的模板模式在 prepare方法中定义了整个方法的框架,然后将一些与子类相关的操作交给其三个子类处理。
SimpleStatementHandler类、PreparedStatementHandler类和 CallableStatementHandler类是三个真正的 Statement 处理器,分别处理 Statement 对象、PreparedStatement 对象和CallableStatement对象。通过其中的 parameterize方法可以看出三个 Statement处理器的不同。

  • SimpleStatementHandler中 parameterize方法的实现为空,因为它只需完成字符串替换即可,不需要进行参数处理。
  • PreparedStatementHandler中 parameterize方法最终通过ParameterHandler接口经过多级中转后调用了java.sql.PreparedStatement类中的参数赋值方法。
  • CallableStatementHandler中 parameterize方法。它一共完成两步工作:一是通过registerOutputParameters方法中转后调用java.sql.CallableStatement中的输出参数注册方法完成输出参数的注册;二是通过 ParameterHandler接口经过多级中转后调用 java.sql.PreparedStatement类中的参数赋值方法。
  /**
   * 对语句进行参数处理
   * @param statement SQL语句
   * @throws SQLException
   */
  @Override
  public void parameterize(Statement statement) throws SQLException {
    
    
    // 输出参数的注册
    registerOutputParameters((CallableStatement) statement);
    // 输入参数的处理
    parameterHandler.setParameters((CallableStatement) statement);
  }

可见 SimpleStatementHandler类、PreparedStatementHandler类和 CallableStatement-Handler类最终是依靠java.sql包下的 Statement接口及其子接口提供的功能完成具体参数处理操作的。

1.3.参数处理功能

为 SQL语句中的参数赋值是 MyBatis进行语句处理时非常重要的一步,而这一步就是由 parameter子包完成的。
parameter子包中其实只有一个 ParameterHandler接口,它定义了两个方法:

  • getParameterObject方法用来获取 SQL语句对应的实参对象。
  • setParameters方法用来完成 SQL语句中的变量赋值。
    ParameterHandler接口有一个默认的实现类DefaultParameterHandler,DefaultParameterHandler在scripting包的 defaults子包中。
public class DefaultParameterHandler implements ParameterHandler {
    
    
  // 类型处理器注册表
  private final TypeHandlerRegistry typeHandlerRegistry;
  // MappedStatement对象(包含完整的增删改查节点信息)
  private final MappedStatement mappedStatement;
  // 参数对象
  private final Object parameterObject;
  // BoundSql对象(包含SQL语句、参数、实参信息)
  private final BoundSql boundSql;
  // 配置信息
  private final Configuration configuration;

MyBatis 中支持进行参数设置的语句类型是 PreparedStatement 接口及其子接口(CallableStatement 是 PreparedStatement 的子接口),所以 setParameters 的输入参数是PreparedStatement类型。

 /**
   * 为语句设置参数
   * @param ps 语句
   */
  @Override
  public void setParameters(PreparedStatement ps) {
    
    
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 取出参数列表
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
    
    
      for (int i = 0; i < parameterMappings.size(); i++) {
    
    
        ParameterMapping parameterMapping = parameterMappings.get(i);
        // ParameterMode.OUT是CallableStatement的输出参数,已经单独注册。故忽略
        if (parameterMapping.getMode() != ParameterMode.OUT) {
    
    
          Object value;
          // 取出属性名称
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) {
    
    
            // 从附加参数中读取属性值
            value = 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);
          }
          // 确定该参数的处理器
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
    
    
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
    
    
            // 此方法最终根据参数类型,调用java.sql.PreparedStatement类中的参数赋值方法,对SQL语句中的参数赋值
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
    
    
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

setParameters 方法的实现逻辑也很简单,就是依次取出每个参数的值,然后根据参数类型调用 PreparedStatement中的赋值方法完成赋值。

1.4.结果处理功能

MyBatis查询结果的处理,需要完成的功能有:

  • 处理结果映射中的嵌套映射等逻辑;
  • 根据映射关系,生成结果对象;
  • 根据数据库查询记录对结果对象的属性进行赋值;
  • 将结果对象汇总为 List、Map、Cursor等形式。

executor包的 result子包只负责完成“将结果对象汇总为 List、Map、Cursor等形式”这一简单功能中的一部分:将结果对象汇总为 List或 Map的形式。而将结果汇总为 Cursor形式的功能由cursor包实现。

在介绍 result子包之前,我们先介绍位于 session包中的两个接口:ResultContext接口和 ResultHandler接口。

  • ResultContext接口表示结果上下文,其中存放了数据库操作的一个结果(对应数据库中的一条记录)。
  • ResultHandler 接口表示结果处理器,数据库操作结果会由它处理。因此说,ResultHandler会负责处理 ResultContext。

result 子包中主要有三个类:DefaultResultContext 类、DefaultResultHandler 类和DefaultMapResultHandler类。这三个类中,DefaultResultContext类是 ResultContext接口唯一的实现类,DefaultResultHandler类和DefaultMapResultHandler类是 ResultHandler接口的实现类。
在这里插入图片描述
DefaultResultContext 用来存储一个结果对象,对应数据库中的一条记录。

public class DefaultResultContext<T> implements ResultContext<T> {
    
    
  // 结果对象
  private T resultObject;
  // 结果计数(表明这是第几个结果对象)
  private int resultCount;
  // 使用完毕(结果已经被取走)
  private boolean stopped;

了解了DefaultResultContext类的各个属性后,对各个ResultHandler类的分析就非常简单了。DefaultResultHandler类负责将 DefaultResultContext类中的结果对象聚合成一个List返回;而 DefaultMapResultHandler类负责将DefaultResultContext类中的结果对象聚合成一个 Map返回。

其中 DefaultMapResultHandler类稍微复杂一些,我们以它为例进行介绍。

public class DefaultMapResultHandler<K, V> implements ResultHandler<V> {
    
    
  // Map形式的映射结果
  private final Map<K, V> mappedResults;
  // Map的键。由用户指定,是结果对象中的某个属性名
  private final String mapKey;
  // 对象工厂
  private final ObjectFactory objectFactory;
  // 对象包装工厂
  private final ObjectWrapperFactory objectWrapperFactory;
  // 反射工厂
  private final ReflectorFactory reflectorFactory;
    /**
   * 处理一个结果
   * @param context 一个结果
   */
  @Override
  public void handleResult(ResultContext<? extends V> context) {
    
    
    // 从结果上下文中取出结果对象
    final V value = context.getResultObject();
    // 获得结果对象的元对象
    final MetaObject mo = MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory);
    // 基于元对象取出key对应的值
    final K key = (K) mo.getValue(mapKey);
    mappedResults.put(key, value);
  }

DefaultMapResultHandler类中的 handleResult方法用来完成Map的组装。

这样,我们对单个结果对象如何被聚合为 List、Map、Cursor 形式返回进行了了解。DefaultResultContext作为默认的ResultContext实现类,存储了一个结果对象,对应着数据库中的一条记录。而 ResultHandler有三个实现类能够处理DefaultResultContext对象,这三个实现类的功能如下。

  • DefaultResultHandler类负责将多个 ResultContext聚合为一个 List返回。
  • DefaultMapResultHandler类负责将多个 ResultContext聚合为一个 Map返回。
  • DefaultCursor 类中的 ObjectWrapperResultHandler 内部类负责将多个 ResultContext聚合为一个 Cursor返回。

1.5.结果集处理功能

1.4节对 MyBatis将单个结果对象聚合为 List、Map、Cursor的机制进行了介绍。但那只是结果处理流程中非常小的一个环节。在结果处理流程中,尚未完成的功能还有:

  • 处理结果映射中的嵌套映射等逻辑;
  • 根据映射关系,生成结果对象;
  • 根据数据库查询记录对结果对象的属性进行赋值。

以上这些功能均由 resultset子包提供。resultset子包提供的功能虽多,但是只有三个类。
在这里插入图片描述

ResultSetWrapper是结果封装类,ResultSetHandler和DefaultResultSetHandler分别是结果集处理器的接口和实现类。
在介绍以上三个类之前,我们先了解 MyBatis的结果集。

1.5.1.MyBatis中多结果集的处理

为了介绍 MyBatis中的结果集处理功能,我们先明确以下概念。

  • 结果:从数据库中查询出的一条记录就是一个结果,它可以映射为一个 Java对象。
  • 结果集:指结果的集合,从数据库中查询出的多个记录就是一个结果集。结果集可以以 List、Map、Cursor的形式返回。
  • 多结果集:即结果集的集合,其中包含了多个结果集。

说到多结果集,大家可能会产生一个疑问:平时每一次数据库查询操作都只会返回一个结果集,怎么会有多结果集的概念?例如,如下代码的 union操作,其实就是把两个结果集进行了合并,但最终两个结果集还是会合并为一个结果集。
在这里插入图片描述
虽然结果集中的结果可以明显地被分为两类,但是它们仍然属于一个结果集。在这个结果集中,每条结果都包含 id和 name这两个属性。

但其实,一次数据库查询确实可以返回一个多结果集。
在这里插入图片描述
在这里插入图片描述
MyBatis也支持处理多结果集,

<resultMap id="userMap" type="User" autoMapping="true">
    <collection property="taskList" resultSet="taskRecord" resultMap="taskMap" />
</resultMap>

<resultMap id="taskMap" type="Task">
    <result property="id" column="id" />
    <result property="userId" column="userId" />
    <result property="taskName" column="taskName" />
</resultMap>

<!--返回两个结果组成的结果集-->
<select id="query" resultMap="userMap,taskMap" resultSets="userRecord,taskRecord" statementType="CALLABLE">
  CALL multiResults()
</select>

语句会接受两个结果集,并将两个结果集分别命名为 userRecord和taskRecord。然后使用 userMap来对结果集 userRecord进行映射,使用 taskMap来对结果集 taskRecord进行映射。
最终可以得到result变量中存储的多结果集,其中的两个结果集均使用 List存储。第一个结果集为使用userMap映射出的 User对象列表,第二个结果集为使用taskMap映射出的 Task对象列表。

1.5.2.结果集封装类

java.sql.Statement进行完数据库操作之后,对应的操作结果是由 java.sql.ResultSet返回的。java.sql.ResultSet接口中定义的方法,它主要分为几大类:

  • 切换到下一结果,读取本结果是否为第一个结果、最后一个结果等结果间切换相关的方法;
  • 读取当前结果某列的值;
  • 修改当前结果某列的值(修改不会影响数据库中的真实值);
  • 一些其他的辅助功能,如读取所有列的类型信息等。
    以上这几类方法已经能够满足数据库结果的查询操作。

而 MyBatis中的 ResultSetWrapper类是对 java.sql.ResultSet的进一步封装,这里用到了装饰器模式。ResultSetWrapper类在 java.sql.ResultSet接口的基础上扩展出了更多的功能,这些功能包括获取所有列名的列表、获取所有列的类型的列表、获取某列的 JDBC类型、获取某列对应的类型处理器等。

 /* 
 * ResultSet的封装,包含了ResultSet的一些元数据
 * 类似于装饰器模式,但是比较简单。只是在原有类的外部再封装一些属性、方法
 */
public class ResultSetWrapper {
    
    
  // 被装饰的resultSet对象
  private final ResultSet resultSet;
  // 类型处理器注册表
  private final TypeHandlerRegistry typeHandlerRegistry;
  // resultSet中各个列对应的列名列表
  private final List<String> columnNames = new ArrayList<>();
  // resultSet中各个列对应的Java类型名列表
  private final List<String> classNames = new ArrayList<>();
  // resultSet中各个列对应的JDBC类型列表
  private final List<JdbcType> jdbcTypes = new ArrayList<>();
  // <列名,< java类型,TypeHandler>>
  // 这里的数据是不断组建起来的。java类型传入,然后去全局handlerMap索引java类型的handler放入map,然后在赋给列名。
  // 每个列后面的java类型不应该是唯一的么?不是的
  //    <resultMap id="userMapFull" type="com.example.demo.UserBean">
  //        <result property="id" column="id"/>
  //        <result property="schoolName" column="id"/>
  //    </resultMap>
  // 上面就可能不唯一,同一个列可以给不同的java属性
  // 类型与类型处理器的映射表。结构为:Map<列名,Map<Java类型,类型处理器>>
  private final Map<String, Map<Class<?>, TypeHandler<?>>> typeHandlerMap = new HashMap<>();
  // 记录了所有的有映射关系的列。
  // key为resultMap的id,后面的List为该resultMap中有映射的列的列表
  // <resultMap的id,List<对象映射的列名>>
  // 记录了所有的有映射关系的列。结构为:Map<resultMap的id,List<对象映射的列名>>
  private final Map<String, List<String>> mappedColumnNamesMap = new HashMap<>();
  // 记录了所有的无映射关系的列。
  // key为resultMap的id,后面的List为该resultMap中无映射的列的列表
  //  // <resultMap的id : List<对象映射的列名>>
  // 记录了所有的无映射关系的列。结构为:Map<resultMap的id,List<对象映射的列名>>
  private final Map<String, List<String>> unMappedColumnNamesMap = new HashMap<>();

1.5.2.结果集处理类

ResultSetHandler是结果集处理器接口,它定义了结果集处理器的三个抽象方法。

  • <E> List<E> handleResultSets(Statement stmt):将Statement的执行结果处理为 List。
  • <E> Cursor<E> handleCursorResultSets(Statement stmt):将 Statement的执行结果处理为 Map。
  • void handleOutputParameters(CallableStatement cs):处理存储过程的输出结果。
    DefaultResultSetHandler类作为 ResultSetHandler接口的默认也是唯一的实现类,实现了上述的抽象方法。下面以 DefaultResultSetHandler类中的 handleResultSets方法为例,介绍 MyBatis如何完成结果集的处理。

通过 handleResultSets方法的名称(Sets为复数形式)也能看出,它能够处理多结果集。在处理多结果集时,我们得到的是两层列表,即结果集列表和嵌套在其中的结果列表,而在处理单结果集时,我们可以直接得到结果列表。
在这里插入图片描述

  /**
   * 处理Statement得到的多结果集(也可能是单结果集,这是多结果集的一种简化形式),最终得到结果列表
   * @param stmt Statement语句
   * @return 结果列表
   * @throws SQLException
   */
  @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);
    // 查询语句对应的resultMap节点,可能含有多个
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    // 合法性校验(存在输出结果集的情况下,resultMapCount不能为0)
    validateResultMapsCount(rsw, resultMapCount);
    // 循环遍历每一个设置了resultMap的结果集
    while (rsw != null && resultMapCount > resultSetCount) {
    
    
      // 获得当前结果集对应的resultMap
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 进行结果集的处理
      handleResultSet(rsw, resultMap, multipleResults, null);
      // 获取下一结果集
      rsw = getNextResultSet(stmt);
      // 清理上一条结果集的环境
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    // 获取多结果集中所有结果集的名称
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
    
    
      // 循环遍历每一个没有设置resultMap的结果集
      while (rsw != null && resultSetCount < resultSets.length) {
    
    
        // 获取该结果集对应的父级resultMap中的resultMapping(注:resultMapping用来描述对象属性的映射关系)
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
    
    
          // 获取被嵌套的resultMap的编号
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          // 处理嵌套映射
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }
    // 判断是否是单结果集:如果是则返回结果列表;如果否则返回结果集列表
    return collapseSingleResultList(multipleResults);
  }

handleResultSets 方法完成了对多结果集的处理。但是对于每一个结果集的处理是由handleResultSet子方法实现的

  /**
   * 处理单一的结果集
   * @param rsw ResultSet的包装
   * @param resultMap resultMap节点的信息
   * @param multipleResults 用来存储处理结果的list
   * @param parentMapping
   * @throws SQLException
   */
  private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    
    
    try {
    
    
      if (parentMapping != null) {
    
     // 嵌套的结果
        // 向子方法传入parentMapping。处理结果中的记录。
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
    
     // 非嵌套的结果
        if (resultHandler == null) {
    
    
          // defaultResultHandler能够将结果对象聚合成一个List返回
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          // 处理结果中的记录。
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
    
    
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
    
    
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

传入 handleResultSet 方法的已经是单结果集,handleResultSet 方法调用了handleRowValues方法进行进一步的处理。
在handleRowValues方法中,会以当前映射中是否存在嵌套为依据再次进行分类,分别调用handleRowValuesForNestedResultMap方法和handleRowValuesForSimpleResultMap方法。

  /**
   * 处理单结果集中的属性
   * @param rsw 单结果集的包装
   * @param resultMap 结果映射
   * @param resultHandler 结果处理器
   * @param rowBounds 翻页限制条件
   * @param parentMapping 父级结果映射
   * @throws SQLException
   */
  public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
                              RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    
    
    if (resultMap.hasNestedResultMaps()) {
    
    
      // 前置校验
      ensureNoRowBounds();
      checkResultHandler();
      // 处理嵌套映射
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
    
    
      // 处理单层映射
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

以 handleRowValuesForSimpleResultMap 方法为例,继续查看整体的处理流程

  /**
   * 处理非嵌套映射的结果集
   * @param rsw 结果集包装
   * @param resultMap 结果映射
   * @param resultHandler 结果处理器
   * @param rowBounds 翻页限制条件
   * @param parentMapping 父级结果映射
   * @throws SQLException
   */
  private 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
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      // 拿到了一行记录,并且将其转化为一个对象
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      // 把这一行记录转化出的对象存起来
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }

在handleRowValuesForSimpleResultMap方法中,真正完成了对结果集中结果的处理。对每一条结果进行处理时,包括以下几个功能。

  • 基于鉴别器获取该条记录对应的 resultMap,该功能调用resolveDiscriminatedResult-Map子方法实现。
  • 根据 resultMap,将这条记录转化为一个对象,该功能调用getRowValue子方法实现。
  • 把由这一行记录转化得到的对象存起来,该功能调用storeObject子方法实现。

getRowValue方法
该方法使用反射创建了记录对应的对象,并给对象的属性进行了赋值。创建对象的操作过程大家可以通过 createResultObject子方法继续追踪,为对象属性赋值的操作过程可以通过applyAutomaticMappings 子方法和applyPropertyMappings子方法继续追踪。

  /**
   * 将一条记录转化为一个对象
   * @param rsw 结果集包装
   * @param resultMap 结果映射
   * @param columnPrefix 列前缀
   * @return 转化得到的对象
   * @throws SQLException
   */
  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    
    
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // 创建这一行记录对应的对象
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
    
    
      // 根据对象得到其MetaObject
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      // 是否允许自动映射未明示的字段
      if (shouldApplyAutomaticMappings(resultMap, false)) {
    
    
        // 自动映射未明示的字段
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      // 按照明示的字段进行重新映射
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

storeObject方法
在storeObject方法中,会根据当前对象的不同分别进行处理:

  • 如果当前对象属于父级映射,则将该对象绑定到父级对象上;
  • 如果当前对象属于独立映射,则使用 ResultHandler聚合该对象。
  /**
   * 存储当前结果对象
   * @param resultHandler 结果处理器
   * @param resultContext 结果上下文
   * @param rowValue 结果对象
   * @param parentMapping 父级结果映射
   * @param rs 结果集
   * @throws SQLException
   */
  private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
    
    
    if (parentMapping != null) {
    
    
      // 存在父级,则将这一行记录对应的结果对象绑定到父级结果上
      linkToParents(rs, parentMapping, rowValue);
    } else {
    
    
      // 使用resultHandler聚合该对象
      callResultHandler(resultHandler, resultContext, rowValue);
    }
  }

这样经过层层子方法调用后便完成了 handleResultSets 方法的源码阅读。可见在handleResultSets方法中,完成了生成结果对象、为结果对象的属性赋值、将结果对象进行聚合或绑定等重要操作。

2.执行器

每个子包都为执行器提供了一些子功能。但是最终这些子功能均由 Executor接口及其实现类串接了起来,共同向外提供服务。
在这里插入图片描述

2.1.执行器接口

首先看一下 Executor接口中定义的方法。

public interface Executor {
    
    
  ResultHandler NO_RESULT_HANDLER = null;
  // 数据更新操作,其中数据的增加、删除、更新均可由该方法实现
  int update(MappedStatement ms, Object parameter) throws SQLException;
  // 数据查询操作,返回结果为列表形式
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
  // 数据查询操作,返回结果为列表形式
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
  // 数据查询操作,返回结果为游标形式
  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
  // 清理缓存
  List<BatchResult> flushStatements() throws SQLException;
  // 提交事务
  void commit(boolean required) throws SQLException;
  // 回滚事务
  void rollback(boolean required) throws SQLException;
  // 创建当前查询的缓存键值
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
  // 本地缓存是否有指定值
  boolean isCached(MappedStatement ms, CacheKey key);
  // 清理本地缓存
  void clearLocalCache();
  // 懒加载
  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
  // 获取事务
  Transaction getTransaction();
  // 关闭执行器
  void close(boolean forceRollback);
  // 判断执行器是否关闭
  boolean isClosed();
  // 设置执行器包装
  void setExecutorWrapper(Executor executor);
}

基于以上方法可以完成数据的增、删、改、查,以及事务处理等操作。而事实上,MyBatis的所有数据库操作也确实是通过调用这些方法实现的。

2.1.执行器基类与实现类

Executor接口的各个实现类中,CachingExecutor并没有包含具体的数据库操作,而是在其他数据库操作的基础上封装了一层缓存,因此它没有继承BaseExecutor。而其他的各个实现类都继承了BaseExecutor。

BaseExecutor 是一个抽象类,并用到了模板模式。它实现了其子类的一些共有的基础功能,而将与子类直接相关的操作交给子类处理。

  /**
   * 查询数据库中的数据
   * @param ms 映射语句
   * @param parameter 参数对象
   * @param rowBounds 翻页限制条件
   * @param resultHandler 结果处理器
   * @param key 缓存的键
   * @param boundSql 查询语句
   * @param <E> 结果类型
   * @return 结果列表
   * @throws SQLException
   */
  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    
    
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
    
    
      // 执行器已经关闭
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
    
     // 新的查询栈且要求清除缓存
      // 清除一级缓存
      clearLocalCache();
    }
    List<E> list;
    try {
    
    
      queryStack++;
      // 尝试从本地缓存获取结果
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
    
    
        // 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
    
    
        // 本地缓存没有结果,故需要查询数据库
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
    
    
      queryStack--;
    }
    if (queryStack == 0) {
    
    
      // 懒加载操作的处理
      for (DeferredLoad deferredLoad : deferredLoads) {
    
    
        deferredLoad.load();
      }
      deferredLoads.clear();
      // 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    
    
        clearLocalCache();
      }
    }
    return list;
  }

在 query方法的核心方法中,会尝试读取一级缓存,而在缓存中无结果时,则会调用queryFromDatabase方法进行数据库中结果的查询。

  /**
   * 从数据库中查询结果
   * @param ms 映射语句
   * @param parameter 参数对象
   * @param rowBounds 翻页限制条件
   * @param resultHandler 结果处理器
   * @param key 缓存的键
   * @param boundSql 查询语句
   * @param <E> 结果类型
   * @return 结果列表
   * @throws SQLException
   */
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    
    
    List<E> list;
    // 向缓存中增加占位符,表示正在查询
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
    
    
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
    
    
      // 删除占位符
      localCache.removeObject(key);
    }
    // 将查询结果写入缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
    
    
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

doQuery子方法的具体实现则交由BaseExecutor的子类实现,因此这是典型的模板模式。

接下来不妨再分析一下 BaseExecutor中 update方法的源码

  /**
   * 更新数据库数据,INSERT/UPDATE/DELETE三种操作都会调用该方法
   * @param ms 映射语句
   * @param parameter 参数对象
   * @return 数据库操作结果
   * @throws SQLException
   */
  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    
    
    ErrorContext.instance().resource(ms.getResource())
            .activity("executing an update").object(ms.getId());
    if (closed) {
    
    
      // 执行器已经关闭
      throw new ExecutorException("Executor was closed.");
    }
    // 清理本地缓存
    clearLocalCache();
    // 返回调用子类进行操作
    return doUpdate(ms, parameter);
  }

可以很明显地看出,update 方法的源码要比 query 方法的源码简单很多。查询操作的源码往往比增加、删除、修改操作的源码复杂的原因是:

  • 查询操作的输入参数相对比较复杂,而增加、删除、修改等操作的参数通常比较简单。
  • 查询操作的输出结果相对比较复杂,结果通常会被映射成对象,甚至还会包括嵌套、结果集操作、懒加载、对象类型鉴别等复杂操作;而增加、删除、修改等操作的输出结果通常比较简单,仅包含影响的条数。
  • 查询操作的输出结果形式比较复杂,如支持列表List、映射表Map、游标 Cursor等形式的输出;而增加、删除、修改等操作的结果形式通常比较简单,仅包含一个数字。
  • 查询操作的实现比较复杂,例如需要进行缓存处理、懒加载处理、嵌套映射处理等;而增加、删除、修改等操作则往往不需要这些复杂的处理。

正因为查询操作比其他操作更为复杂,所以在本书的源码阅读中经常以查询操作为例进行源码解析。在源码阅读时,往往会遇到很多分支。这些分支在实现思路上是相似的,我们没有必要将它们的源码全部进行阅读,只需选取其中一些有代表性的分支深入阅读即可。在选择分支的过程中,有以下两个思路。

  1. 可以考虑选择最为复杂的分支,这样当这个分支的代码被我们分析清楚时,其他分支的代码自然也就清楚了
  2. 也可以考虑选择最为简单的分支,这样能让我们快速读懂代码的思路,再去分析其他复杂分支也会更加容易。
    具体遵循哪个思路进行分支的选择要根据具体情况来分析。通常来说,对于重要的代码选择复杂的分支;对于次要的代码选择简单的分支;对于简单的代码选择复杂的分支;对于复杂的代码选择简单的分支。
    对于 MyBatis而言,输入/输出参数的处理、缓存的处理、懒加载的处理等都是一些非常重要的功能。因此,我们会选择包含这些功能的查询操作分支展开源码阅读。

BaseExecutor有四个实现类,其功能分别如下。

  • ClosedExecutor:一个仅能表征自身已经关闭的执行器,没有其他实际功能。
  • SimpleExecutor:一个最为简单的执行器。
  • BatchExecutor:支持批量执行功能的执行器。
  • ReuseExecutor:支持 Statement对象复用的执行器。
    SimpleExecutor、BatchExecutor、ReuseExecutor 这三个执行器的选择是在MyBatis的配置文件中进行的,可选的值由session包中的枚举类 ExecutorType定义。这三个执行器主要基于 StatementHandler完成创建 Statement对象、绑定参数等工作。
    BatchResult也是 executor中的一个类,它可以保存批量操作的参数对象列表和影响条数列表。

3.错误上下文

在很多方法的开始阶段都会调用 ErrorContext 类的相关方法。

    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());

其中的 ErrorContext 类是一个错误上下文,它能够提前将一些背景信息保存下来。这样在真正发生错误时,便能将这些背景信息提供出来,进而给我们的错误排查带来便利。

public class ErrorContext {
    
    
  // 获得当前操作系统的换行符
  private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
  // 将自身存储进ThreadLocal,从而进行线程间的隔离
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
  // 存储上一版本的自身,从而组成错误链
  private ErrorContext stored;
  // 下面几条为错误的详细信息,可以写入一项或者多项
  private String resource;
  private String activity;
  private String object;
  private String message;
  private String sql;
  private Throwable cause;

ErrorContext 类的属性设置非常简单,但是整个类却设计得非常巧妙。首先,

  /**
   * 从ThreadLocal取出已经实例化的ErrorContext,或者实例化一个ErrorContext放入ThreadLocal
   * @return ErrorContext实例
   */
  public static ErrorContext instance() {
    
    
    ErrorContext context = LOCAL.get();
    if (context == null) {
    
    
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }

ErrorContext类实现了单例模式,而它的单例是绑定到 ThreadLocal上的。这保证了每个线程都有唯一的一个错误上下文 ErrorContext。
ErrorContext 类还有一种包装机制,即每个 ErrorContext 对象内可以包装一个ErrorContext对象。这样,错误上下文就可以组成一条错误链,这和前面介绍的异常链十分类似。该包装功能由 store方法实现:

  /**
   * 创建一个包装了原有ErrorContext的新ErrorContext
   * @return 新的ErrorContext
   */
  public ErrorContext store() {
    
    
    ErrorContext newContext = new ErrorContext();
    newContext.stored = this;
    LOCAL.set(newContext);
    return LOCAL.get();
  }

当然,除了能够创建一个包装了原有 ErrorContext 对象的新ErrorContext 对象外,ErrorContext类还支持这种操作的逆操作——将某个 ErrorContext对象的内部 ErrorContext对象剥离出来。该剥离功能由 recall方法实现:

  /**
   * 剥离出当前ErrorContext的内部ErrorContext
   * @return 剥离出的ErrorContext对象
   */
  public ErrorContext recall() {
    
    
    if (stored != null) {
    
    
      LOCAL.set(stored);
      stored = null;
    }
    return LOCAL.get();
  }

除此之外,ErrorContext类中还有用来清除所有信息的 reset方法、用来转化为字符串输出的 toString方法,以及用来设置各个详细信息的 instance、resource、activity、store等方法。这些方法的使用场景如下。

  • 当需要获得当前线程的 ErrorContext对象时,调用 instance方法。
  • 当线程执行到某一阶段产生了新的上下文信息时,调用resource、activity等方法向ErrorContext对象补充上下文信息。
  • 当线程进入下一级操作并处于一个全新的环境时,调用 store 方法获得一个包装了原有 ErrorContext对象的新 ErrorContext对象。
  • 当线程从下一级操作返回上一级时,调用 recall方法剥离上一级的 ErrorContext对象。
  • 当线程进入一个与之前操作无关的新环境时,调用 reset方法清除 ErrorContext对象的所有信息。
  • 当线程需要打印异常信息时,调用 toString方法输出错误发生时的环境信息。
    通过这些操作,线程的 ErrorContext 类中时刻保存着当前时刻的上下文信息,一旦真正发生异常便可以把这些信息提供出来。
    在这里插入图片描述

wrapException方法(该方法包含在 exceptions包的 ExceptionFactory类中),只是显式地更新了 ErrorContext对象中的 message属性和 cause属性,但toString方法输出的结果中可能包含更为丰富的属性信息。而那些属性信息是随着线程执行环境的变化而实时更新的。

猜你喜欢

转载自blog.csdn.net/d495435207/article/details/131342823