4

阅读 MyBatis 源码:SQL 执行过程

 2 years ago
source link: https://segmentfault.com/a/1190000040610536
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇文章介绍了 JDBC 实现 SQL 查询的原理 ,本文通过一个简单的 MyBatis 查询示例,探究 MyBatis 执行 SQL 查询的代码流程。

本文基于 MyBatis 3.5.7。

1. 使用示例

工程结构:
mybatis-intro
简单查询例子:

private static SqlSessionFactory sqlSessionFactory;

@BeforeClass
public static void init() {
    try {
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@Test
public void selectAll() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        List<Student> students = sqlSession.selectList("selectAll");
        for (int i = 0; i < students.size(); i++) {
            System.out.println(students.get(i));
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2. 源码分析

2.1 解析配置文件

2.1.1 解析 mybatis-config.xml

MyBatis XML 配置文件例子如下,更多说明见官方文档-XML配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- MyBatis XML 配置说明 https://mybatis.org/mybatis-3/zh/configuration.html -->

    <!-- 引入其他配置文件 -->
    <properties resource="jdbc.properties"/>

    <settings>
        <!-- 设置一级缓存的范围 -->
        <setting name="localCacheScope" value="SESSION"/>
        <!-- 开启缓存 -->
        <setting name="cacheEnabled" value="true"/>
        <!-- 开启驼峰式命名,数据库的列名能够映射到去除下划线驼峰命名后的字段名-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找 -->
        <setting name="logImpl" value="LOG4J"/>
    </settings>

    <!-- 类型别名可为 Java 类型设置一个缩写名字。它仅用于 XML 配置,意在降低冗余的全限定类名书写 -->
    <typeAliases>
        <!-- 指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean。在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名 -->
        <package name="com.sumkor.entity"/>
    </typeAliases>

    <plugins>
        <plugin interceptor="com.sumkor.plugin.StatementInterceptor"/>
        <plugin interceptor="com.sumkor.plugin.PageInterceptor"/>
    </plugins>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC">
                <property name="" value=""/>
            </transactionManager>
            <!-- POOLED 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!-- 使用 package 标签,需要把 xml 映射文件和 Mapper 接口文件放在同一个目录,而且必须同名 -->
        <package name="com.sumkor.mapper"/>
        <!-- <mapper resource="com/sumkor/mapper/StudentMapper.xml"/> -->
    </mappers>
</configuration>

通过 SqlSessionFactoryBuilder 来解析 mybatis-config.xml 文件:

Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

这里会把 mybatis-config.xml 配置文件解析成 org.apache.ibatis.session.Configuration 对象,这是一个重要的配置类。

org.apache.ibatis.session.SqlSessionFactoryBuilder#build
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

解析 mybatis-config.xml 中的每一个节点。

org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration

  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins")); // 解析插件配置
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments")); // 解析环境配置:数据源、事务管理
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers")); // 解析映射文件
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

解析得到 Configuration 对象之后,可以用它来构造 SqlSessionFactory 对象。

org.apache.ibatis.session.SqlSessionFactoryBuilder#build(org.apache.ibatis.session.Configuration)

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

2.1.2 解析 SQL 映射文件

SQL 映射文件 com\sumkor\mapper\StudentMapper.xml 示例如下,更多说明见官方文档-XML映射

<mapper namespace="com.sumkor.mapper.StudentMapper">
    <!-- 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以必须在参数和结果映射中指明字段的 jdbcType 类型,以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型 -->
    <resultMap id="BaseResultMap" type="com.sumkor.entity.Student">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="phone" jdbcType="VARCHAR" property="phone"/>
        <result column="email" jdbcType="VARCHAR" property="email"/>
        <result column="sex" jdbcType="TINYINT" property="sex"/>
        <result column="locked" jdbcType="TINYINT" property="locked"/>
        <result column="gmt_created" jdbcType="TIMESTAMP" property="gmtCreated"/>
        <result column="gmt_modified" jdbcType="TIMESTAMP" property="gmtModified"/>
        <!-- 支持的 jdbcType 见 org.apache.ibatis.type.JdbcType 或者 https://mybatis.org/mybatis-3/zh/sqlmap-xml.html -->
    </resultMap>

    <sql id="base_column_list">
        id, name, phone, email, sex, locked, gmt_created, gmt_modified
    </sql>

    <select id="selectAll" resultMap="BaseResultMap">
        select
        <include refid="base_column_list"/>
        from student
    </select>
</mapper>    

在解析 mybatis-config.xml 文件的时候,解析 mappers 标签,找到对应的 SQL 映射文件。

org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
org.apache.ibatis.session.Configuration#addMappers
org.apache.ibatis.binding.MapperRegistry#addMapper
org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement

  private 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);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

SQL 映射文件中一个 <select|update|delete|insert> 节点,会被解析为 MappedStatement 对象。

org.apache.ibatis.mapping.MappedStatement

// 该对象表示 Mapper.xml 中的一条 SQL 信息,相关标签见 org\apache\ibatis\builder\xml\mybatis-3-mapper.dtd
public final class MappedStatement {     

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;             // 这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。默认值为未设置(unset)(依赖驱动)。
  private Integer timeout;               // 驱动程序等待数据库返回请求结果的秒数,超时将会抛出异常
  private StatementType statementType;   // 参数可选值为 STATEMENT、PREPARED 或 CALLABLE,这会让 MyBatis 分别使用 Statement、PreparedStatement 或 CallableStatement 与数据库交互,默认值为 PREPARED
  private ResultSetType resultSetType;   // 参数可选值为 FORWARD_ONLY、SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE,用于设置从结果集读取数据时,读指针能否上下移动。例如,只需要顺序读取,可设置为 FORWARD_ONLY,便于释放已读内容所占的内存
  private SqlSource sqlSource;           // sql 语句
  private Cache cache;                   // 二级缓存,若无配置则为空
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;    // 对应 xml 中的 flushCache 属性。将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
  private boolean useCache;              // 对应 xml 中的 useCache 属性。将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
  private boolean resultOrdered;         // 这个设置仅针对嵌套结果 select 语句。默认值:false。
  private SqlCommandType sqlCommandType; // sql 语句的类型,如 select、update、delete、insert
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;

生成 MappedStatement 对象之后,会将其注册到 Configuration 对象之中:

org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement
org.apache.ibatis.session.Configuration#addMappedStatement

  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());

  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }

Configuration 对象中的 mappedStatements 属性是一个 StrictMap 类型,是 MyBatis 的一个内部类。
可以看到,这里以 MappedStatement#id 进行注册,会同时注册一个短名称(eg:selectAll)和长名称(eg:com.sumkor.mapper.StudentMapper.selectAll)。

org.apache.ibatis.session.Configuration.StrictMap#put

    @Override
    @SuppressWarnings("unchecked")
    public V put(String key, V value) {
      if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key
            + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
      }
      if (key.contains(".")) {
        final String shortKey = getShortName(key);
        if (super.get(shortKey) == null) {
          super.put(shortKey, value); // 以短名称进行注册
        } else {
          super.put(shortKey, (V) new Ambiguity(shortKey)); // 短名称注册出现冲突
        }
      }
      return super.put(key, value); // 以全名称注册
    }

2.2 开启会话

SqlSession sqlSession = sqlSessionFactory.openSession()

对于 SqlSession 的官方说明:

SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
在 Web 应用中,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。

构建 SqlSession 实例,代码如下。默认 autoCommit 为 false。
注意,每一个 SqlSession 会话都有独自的 Executor 和 Transaction 实例。

org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 通过事务工厂,实例化 Transaction 事务对象
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); 
      // 实例化 Executor 执行器对象,通过它来执行 SQL,支持插件扩展
      final Executor executor = configuration.newExecutor(tx, execType); 
      // 构建 SqlSession 
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

默认使用 SimpleExecutor 作为执行器,作为 MyBatis 的调度中心。

org.apache.ibatis.session.Configuration#newExecutor

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。
    if (ExecutorType.BATCH == executorType) {        
      executor = new BatchExecutor(this, transaction);
    } 
    // 该类型的执行器会复用预处理语句。
    else if (ExecutorType.REUSE == executorType) { 
      executor = new ReuseExecutor(this, transaction);
    } 
    // 该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。
    else {                                         
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 使用插件来包装 executor
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

2.3 执行 SQL

List<Student> students = sqlSession.selectList("selectAll");
  • 全限定名(比如 “com.sumkor.mapper.StudentMapper.selectAll)将被直接用于查找及使用。
  • 短名称(比如 “selectAll”)如果全局唯一也可以作为一个单独的引用。如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAll” 和 “com.bar.selectAll”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名。

这里通过字符串 selectAll 从 Configuration 对象之中,查找到对应的 MappedStatement 对象。

org.apache.ibatis.session.defaults.DefaultSqlSession#selectList

  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement); // 根据 SQL id 从配置类中获取 SQL 解析后的对象
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler); // 执行 SQL
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

2.3.1 Executor#query

查询数据库之前,会先查询 MyBatis 缓存。这里只关注数据库查询的部分:

org.apache.ibatis.executor.CachingExecutor#query
org.apache.ibatis.executor.BaseExecutor#query
org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
org.apache.ibatis.executor.SimpleExecutor#doQuery

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 创建 StatementHandler,支持插件扩展
      stmt = prepareStatement(handler, ms.getStatementLog()); // 获取数据库连接对象 Connection,以构造 Statement 对象
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

Executor 对象的主要功能是创建并使用 StatementHandler 访问数据库,并将查询结果存入缓存中(如果配置了缓存的话)。

2.3.2 Executor#prepareStatement

Executor 在创建得到 StatementHandler 对象之后,利用 StatementHandler#prepare 来构建 Statement 对象。

org.apache.ibatis.executor.SimpleExecutor#prepareStatement

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog); // 获取数据库连接对象
    stmt = handler.prepare(connection, transaction.getTimeout()); // 使用 Connection 对象构造 Statement 对象
    handler.parameterize(stmt); // 其中会使用 ParameterHandler 设置参数,支持插件扩展
    return stmt;
  }

org.apache.ibatis.executor.statement.BaseStatementHandler#prepare
org.apache.ibatis.executor.statement.BaseStatementHandler#instantiateStatement
org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement

  protected Statement instantiateStatement(Connection connection) throws SQLException {
    // ...
    String sql = boundSql.getSql();
    return connection.prepareStatement(sql);
  }

到了 JDBC 层面,通过 Connection 来创建 Statement 对象,最后获得 ClientPreparedStatement 实例。

java.sql.Connection#prepareStatement(java.lang.String, int, int)
com.mysql.cj.jdbc.ConnectionImpl#prepareStatement
com.mysql.cj.jdbc.ConnectionImpl#clientPrepareStatement(java.lang.String, int, int, boolean)
com.mysql.cj.jdbc.ClientPreparedStatement#getInstance(com.mysql.cj.jdbc.JdbcConnection, java.lang.String, java.lang.String)

protected static ClientPreparedStatement getInstance(JdbcConnection conn, String sql, String db) throws SQLException {
    return new ClientPreparedStatement(conn, sql, db);
}

2.3.3 StatementHandler#query

获取得到 Statement 对象之后,利用 StatementHandler#query 方法来执行 SQL 语句。

org.apache.ibatis.executor.SimpleExecutor#doQuery
org.apache.ibatis.executor.statement.RoutingStatementHandler#query
org.apache.ibatis.executor.statement.PreparedStatementHandler#query

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }

到了 JDBC 层面,交由 MySQL 驱动中的 PreparedStatement 对象来执行 SQL。

org.apache.ibatis.logging.jdbc.PreparedStatementLogger#invoke
com.mysql.cj.jdbc.ClientPreparedStatement#execute

2.4 结果映射

在 StatementHandler#query 中通过 PreparedStatement 执行完 SQL 之后,向 MySQL 服务器读取响应。

org.apache.ibatis.executor.SimpleExecutor#doQuery
org.apache.ibatis.executor.statement.RoutingStatementHandler#query
org.apache.ibatis.executor.statement.PreparedStatementHandler#query

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }

通过 JDBC 从数据库读取到的是二进制数据,封装在 ResultSet 对象中。
需要将 ResultSet 中的数据,转换成配置在 SQL 映射文件的 ResultMap 对象。

本例中 ResultMap 配置如下,更多配置说明见官方文档-XML映射

<mapper namespace="com.sumkor.mapper.StudentMapper">
    <!-- 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以必须在参数和结果映射中指明字段的 jdbcType 类型,以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型 -->
    <resultMap id="BaseResultMap" type="com.sumkor.entity.Student">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="phone" jdbcType="VARCHAR" property="phone"/>
        <result column="email" jdbcType="VARCHAR" property="email"/>
        <result column="sex" jdbcType="TINYINT" property="sex"/>
        <result column="locked" jdbcType="TINYINT" property="locked"/>
        <result column="gmt_created" jdbcType="TIMESTAMP" property="gmtCreated"/>
        <result column="gmt_modified" jdbcType="TIMESTAMP" property="gmtModified"/>
        <!-- 支持的 jdbcType 见 org.apache.ibatis.type.JdbcType 或者 https://mybatis.org/mybatis-3/zh/sqlmap-xml.html -->
    </resultMap>
</mapper>    

代码流程:

  1. 从数据库读取的响应数据 ResultSet 对象。
  2. 获取配置在 SQL 映射文件的 ResultMap 对象,生成对应的 Java 实体对象。
  3. 利用 ResultSet 对象中的数据,对 Java 实体对象的属性进行赋值。

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets

  @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); // 从数据库读取的响应数据 ResultSet 对象

    List<ResultMap> resultMaps = mappedStatement.getResultMaps(); // 配置在 SQL 映射文件的 ResultMap 对象
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null); // 生成 Java 实体对象并进行属性赋值,存储在 multipleResults
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets(); // (在使用存储过程的情况下)如果配置了多结果集,则进一步处理
    if (resultSets != null) {
      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);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }

值得注意的是,并不是所有在 ResultSet 中的二进制数据,都需要转换成 Java 实体对象。

  1. 使用 RowBounds 的情况下,说明用到了 MyBatis 提供的通用分页功能。只需要取 ResultSet 的 offset 到 limit 范围的数据进行对象映射。
  2. 使用 Discriminator 的情况下,需要根据鉴别器的条件,进行对象映射。

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSet
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValues
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForSimpleResultMap

  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    // 在查询数据库时,如果没有 limit 语句,则 ResultSet 中会包含所有满足条件的数据
    ResultSet resultSet = rsw.getResultSet(); 
    // 把 offset 之前的数据都 skip 掉
    skipRows(resultSet, rowBounds); 
    // 判断数据是否超过了 limit
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { 
      // 如果配置了鉴别器 Discriminator,则对查询的结果进行分支处理
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); 
      // 将 ResultSet 中的一行数据转换为 ResultMap 的一个对象并赋值
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null); 
      // 将结果存储到 ResultHandler 之中
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); 
    }
  }

关于 ResultMap 中的鉴别器 Discriminator 和多结果集的说明,见官方文档-XML映射

2.4.1 数据对象映射

  1. 根据 ResultMap 获取 Java 实体对象。
  2. 为 Java 实体对象的属性赋值。

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue

  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); // 获取 Java 实体类的实例,属性均为空
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue); // 反射工具类 MetaObject
      boolean foundValues = this.useConstructorMappings;
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; // 利用 MetaObject 为 Java 实体的属性赋值
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

2.4.2 根据 ResultMap 获取 Java 实体对象

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createResultObject

  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
      throws SQLException {
    final Class<?> resultType = resultMap.getType(); // Java 实体类型
    final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory); // 反射工具类 MetaClass
    final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
    if (hasTypeHandlerForResultObject(rsw, resultType)) {
      return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
    } else if (!constructorMappings.isEmpty()) {
      return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
    } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
      return objectFactory.create(resultType); // 创建 Java 实体类的实例
    } else if (shouldApplyAutomaticMappings(resultMap, false)) {
      return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs);
    }
    throw new ExecutorException("Do not know how to create an instance of " + resultType);
  }

2.4.3 为 Java 实体对象的属性赋值

从 ResultSet 中读取每一个字段的值,再赋值给 Java 实体的属性。

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#applyPropertyMappings

  private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    final List<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); // 表字段名,例如:gmt_created
      if (propertyMapping.getNestedResultMapId() != null) {
        // the user added a column attribute to a nested result map, ignore it
        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); // 获取字段的值
        // issue #541 make property optional
        final String property = propertyMapping.getProperty(); // 实体属性名,例如:gmtCreated
        if (property == null) {
          continue;
        } else if (value == DEFERRED) {
          foundValues = true;
          continue;
        }
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
          // gcode issue #377, call setter on nulls (value is not 'found')
          metaObject.setValue(property, value); // 为 Java 实体对象的字段赋值
        }
      }
    }
    return foundValues;
  }

至于如何从 ResultSet 中读取一个字段的值,这里是用到了 TypeHandler,做到 java 数据类型和 jdbc 数据类型之间的映射和转换。

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#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 {
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler(); // 获取字段映射所需的 TypeHandler
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); // 获取数据库表的字段名
      return typeHandler.getResult(rs, column); // 进行 java 数据类型和 jdbc 数据类型之间的映射和转换
    }
  }

以 IntegerTypeHandler 为例,从 ResultSet 之中读取字段名 columnName 对应的数据,该字段的数据类型为 int。

org.apache.ibatis.type.IntegerTypeHandler#getNullableResult

  @Override
  public Integer getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    int result = rs.getInt(columnName);
    return result == 0 && rs.wasNull() ? null : result;
  }

MyBatis 执行 SQL 的流程:

  1. 加载配置:将 MyBatis XML 配置文件解析为 Configuration 对象,其中,会将 SQL 映射文件中的 SQL 语句解析为 MappedStatement 对象,存储在 Configuration 对象中。
  2. SQL 解析:通过 SQLSession 接收到请求时,根据传入的 SQL id 找到对应的 MappedStatement 对象,该对象中包含解析后的 SQL 语句。
  3. SQL 执行:由 Executor 进行调度,利用 StatementHandler 向数据库发起查询,底层是使用 JDBC 的 Statement#execute 方法来查询数据库,得到查询结果为 ResultSet 对象。
  4. 结果映射:按照映射的配置对查询结果进行转换,可以转换成 HashMap、JavaBean 或者基本数据类型,并将最终结果返回。

作者:Sumkor
链接:https://segmentfault.com/a/11...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK