coding……
但行好事 莫问前程

Mybatis源码解读『select查询——使用SqlSession』

上篇文章我们介绍了使用Mapper接口查询的实现原理,即Mybatis帮我们生成了代理对象,在代理逻辑中,最终使用SqlSession实现查询。除了使用Mapper接口查询之外,我们也可以直接使用Mybatis的SqlSession提供的方法实现数据库查询。本篇文章,我们就来介绍一下SqlSession查询的底层实现。

我们一般通过如下方式,使用SqlSession完成查询:

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
    String nameSpace = "com.zhuoli.service.mybatis.explore.mapper.UserInfoMapper" + ".";

    System.out.println("query all user");
    List<UserInfo> userInfos = sqlSession.selectList(nameSpace + "queryAllUser");
    userInfos.forEach(System.out::println);
    System.out.println("---------------------");

    System.out.println("queryUserInfoById id = 1");
    UserInfo userInfo = sqlSession.selectOne(nameSpace + "queryUserInfoById", 1L);
    System.out.println(userInfo);
    System.out.println("---------------------");
} finally {
    sqlSession.close();
}

大致步骤如下:

  • 创建SqlSessionFactory
  • 创建SqlSession
  • 确定需要执行的sql的statementId以及参数
  • 调用SqlSession的方法(select**)执行查询并获取查询结果

关于SqlSession的构建,上篇文章Mybatis源码解读『select查询——使用Mapper接口』已经介绍过,这里不再重复介绍了。

待执行的sql是我们再SQL映射文件中配置的,如何解析为Statement的,在Mybatis源码解读『SQL映射文件解析』中也有介绍。

接下来我们就来重点看SqlSession的select方法实现,接下来以selectOne方法为例。

1. 基础类梳理

之前介绍Mybatis的文章中,新增了很多类,以及在使用时需要初始化一些类,可能会有些混乱,我们先来梳理一下。

1.1 DefaultSqlSessionFactory

之前使用Mybatis的示例中,我们首先通过SqlSessionFactoryBuilder的build方法构建了一个DefaultSqlSessionFactory对象,DefaultSqlSessionFactory持有一个Mybatis配置文件解析结果Configuration成员,如下:

1.2 DefaultSqlSession

1.1步获取DefaultSqlSessionFactory后,通过调用DefaultSqlSessionFactory的openSession方法获取了一个DefaultSqlSession对象,DefaultSqlSession类内部有个重要的成员——executor,可以执行sql语句。

除了上述DefaultSqlSessionFactory和DefaultSqlSession,还有一些比较重要的类,我们接下来一一介绍一下。

1.3 Configuration

Configuration是Mybatis配置文件以及SQL映射文件的最终解析结果存储的位置,核心成员如下:

1.3.1 Environment

关于Configuration配置类,有几个比较重要的成员值得关注,首先是Environment。我们是通过Mybatis配置文件<environment>节点配置的环境信息,解析结果就存储在Environment中,如下:

1.3.2 MappedStatement

Configuration类中还有一个比较重要的成员,mappedStatements。类型为Map,用于存储SQL映射文件(**Mapper.xml)中所有的<select>|<insert>|<update>|<delete>配置。一个节点(<select>|<insert>|<update>|<delete>)的解析结果就是一个MappedStatement对象,所有的解析结果都会存储在Configuration类的mappedStatements中。

1.3.2.1 BoundSql

MappedStatement中有一个重要的成员sqlSource,类型为SqlSource,该类的一个重要作用就是获取BoundSql。BoundSql主要作用于sql执行阶段,存储了sql语句、参数以及参数映射等信息,可以说通过BoundSql可以得到一个完整的可执行的sql语句。BoundSql细节如下:

1.3.3 ResultMap

我们在SQL映射文件中,可以配置很多<resultMap>节点,用于描述如何从数据库结果集中加载对象。每个<resultMap>节点会解析为一个ResultMap对象,所有的<resultMap>节点的解析结果会以Map的结构存储在Configuration类的resultMaps成员中。

2. DefaultSqlSession#selectOne

完成上述基础梳理后,我们继续来看如果通过SqlSession提供的方法实现查询。下面来看一下入口selectOne方法。

public <T> T selectOne(String statement, Object parameter) {
  // 调用selectList,获取查询结果,如果selectList返回结果数目大于1,抛异常
  List<T> list = this.selectList(statement, parameter);
  if (list.size() == 1) {
    return list.get(0);
  } else if (list.size() > 1) {
    throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
  } else {
    return null;
  }
}

public <E> List<E> selectList(String statement, Object parameter) {
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    // 根据MappedStatement的Id获取MappedStatement
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 调用执行器executor的query方法执行查询逻辑
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

所以,sql查询都是通过执行器Executor完成的,之前的文章Mybatis源码解读『select查询——使用Mapper接口』中的介绍,我们知道默认创建的执行器类型为SimpleExecutor,同时因为Mybatis默认开启了一级缓存(SqlSession级别,在Mybatis配置文件的<settings>节点cacheEnabled属性配置),所以会通过装饰器模式生成CachingExecutor执行器。这里先不考虑手动配置了插件的情况,配置了插件也还是对CachingExecutor的代理,最终还是CachingExecutor的query方法生效,所以直接来看CachingExecutor的query方法。

# CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 获取BoundSql
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  // 创建CacheKey
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  // 调用重载方法查询
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

关于通过MappedStatement获取BoundSql的逻辑比较繁琐,涉及到动态sql一些列标签的解析和替换(比如${},动态sql标签<if>等),这里不详细介绍了,我们只需要知道通过MappedStatement的getBoundSql方法可以获取当前执行的sql对应的BoundSql。BoundSql上面1.3.2.1节也介绍了,这里直接把图再贴一遍。

CacheKey跟Mybatis的缓存体系有关,在重载的query方法中有使用。接下来重点来看一下重载的query方法。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // 从MappedStatement中获取方法可使用的缓存配置
  Cache cache = ms.getCache();
  // 如果缓存配置不为null(即SQL映射文件中配置了二级缓存),则尝试从缓存中获取
  if (cache != null) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // 从二级缓存中获取sql执行结果
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 若缓存未命中,则调用被装饰执行器(SimpleExecutor)的query方法,获取sql查询结果
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 如果未配置二级缓存,直接使用SimpleExecutor执行sql查询
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

可以看到这里实际查询数据库前,二级缓存生效了。但SqlSession级别的一级缓存还未起作用,所以在Mybatis的缓存体系中,二级缓存是先效的,其次才是一级缓存,最后才是数据库

继续来看SimpleExecutor的query方法,该方法是继承自SimpleExecutor的父类BaseExecutor。

//BaseExecutor
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) {
      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();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

查询数据库的逻辑在方法queryFromDatabase中,继续来看一下:

// BaseExecutor
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);
  // 如果statementType类型为CALLABLE,则向参数缓存中缓存参数数据
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }

  // 返回数据库查询结果
  return list;
}

很明确,实际完成查询数据库的动作在doQuery方法中,doQuery方法在BaseExecutor类中是个抽象方法,在SimpleExecutor类中给予了实现,如下:

// SimpleExecutor
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    // 获取Mybatis配置解析结果Configuration对象
    Configuration configuration = ms.getConfiguration();
    // 创建StatementHandler
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // 创建Statement
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 通过StatementHandler执行sql查询逻辑
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

看到Statement这个类,可以知道,我们离真相很接近了,因为我们之前的文章Mybatis源码解读『Java操作Mysql』使用JDBC实现查询逻辑中也使用到了这个类。

这里Mybatis创建了一个StatementHandler对象,并且查询也是通过这个对象完成的。所以我们接下来分析一下StatementHandler以及Statement是如何实现查询的。

2.1 创建StatementHandler

StatementHandler类是通过Configuration类的newStatementHandler方法创建的。

// Configuration
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  // 创建RoutingStatementHandler
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  // 将<plugins>下配置的所有<interceptor>一一应用到RoutingStatementHandler,这里跟CachingExecutor应用interceptorChain的逻辑一致
  // 也是通过一层层代理完成的
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

所以可以看出RoutingStatementHandler其实是个StatementHandler的装饰器,也可以说是使用了装饰器模式,内部的delegate成员才是真正实现功能的StatementHandler。RoutingStatementHandler从名称上看的意思是”具有路由功能的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());
  }

}

构造函数,可以看出是根据MappedStatement的statementType确定其成员delegate的类型的。通过之前的文章我们知道statementType默认类型为PREPARED。

StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));

所以newStatementHandler创建的StatementHandler类型RoutingStatementHandler,RoutingStatementHandler是一个装饰器,其代表的实际类型是PreparedStatementHandler

2.2 创建Statement

Statement对象是通过prepareStatement方法创建的。

// SimpleExecutor
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  //获取数据库连接
  Connection connection = getConnection(statementLog);
  // 创建Statement
  stmt = handler.prepare(connection, transaction.getTimeout());
  //为PreparedStatement设置运行时参数
  handler.parameterize(stmt);
  return stmt;
}

2.2.1 获取数据库连接

// SimpleExecutor#getConnection 继承自BaseExecutor类
protected Connection getConnection(Log statementLog) throws SQLException {
  // 通过Transaction来获取数据库连接Connection
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  } else {
    return connection;
  }
}

SimpleExecutor类的transaction成员类型为JdbcTransaction(通过<environment>下<transactionManager>的type属性配置的,之前的文章介绍过)。

// JdbcTransaction
public Connection getConnection() throws SQLException {
  // 如果JdbcTransaction成员connection为null,则从DataSource获取一个Connection连接,并赋值给connection成员
  if (connection == null) {
    openConnection();
  }
  return connection;
}

关于从DataSource获取数据库连接的细节,由于篇幅原因,我们下篇文章再单独介绍。

2.2.2 创建Statement

SimpleExecutor中,Statement是通过调用RoutingStatementHandler类的prepare创建的。

// RoutingStatementHandler
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  return delegate.prepare(connection, transactionTimeout);
}

通过上面的介绍,我们知道RoutingStatementHandler是个装饰器,delegate实际类型为PreparedStatementHandler。

// PreparedStatementHandler#prepare 继承自BaseStatementHandler类
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  ErrorContext.instance().sql(boundSql.getSql());
  Statement statement = null;
  try {
    // 实例化Statement
    statement = instantiateStatement(connection);
    // 为Statement设置超时时间
    setStatementTimeout(statement, transactionTimeout);
    // 为Statement设置fetchSize
    setFetchSize(statement);
    return statement;
  } catch (SQLException e) {
    closeStatement(statement);
    throw e;
  } catch (Exception e) {
    closeStatement(statement);
    throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
  }
}
// PreparedStatementHandler
protected Statement instantiateStatement(Connection connection) throws SQLException {
  // 从BoundSql中获取待执行的sql,sql中带有占位符,比如"select * from user_info where id = ?"
  String sql = boundSql.getSql();

  //以下根据条件,调用不同的prepareStatement方法创建PreparedStatement

  // 如果<settings>配置了useGeneratedKeys属性true,则keyGenerator对于insert类型为Jdbc3KeyGenerator,其它类型sql为NoKeyGenerator
  if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
    // 如果对于insert类型sql,并且配置了useGeneratedKeys属性true
    String[] keyColumnNames = mappedStatement.getKeyColumns();
    if (keyColumnNames == null) {
      return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
    } else {
      return connection.prepareStatement(sql, keyColumnNames);
    }
  } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
    // resultType默认为ResultSetType.DEFAULT,所以会进入该分支
    return connection.prepareStatement(sql);
  } else {
    return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
  }
}

MappedStatement的resultSetType属性默认为ResultSetType.DEFAULT,所以创建Statement时,会调用如下方法:

// ConnectionImpl
public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException {
    return prepareStatement(sql, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY);
}

到这里,其实就跟我们之前使用JDBC的方式重合了,使用JDBC查询时,我们也是通过Connection创建的Statement。

// ConnectionImpl
public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
    synchronized (getConnectionMutex()) {
        checkClosed();

        //
        // FIXME: Create warnings if can't create results of the given type or concurrency
        //
        ClientPreparedStatement pStmt = null;

        boolean canServerPrepare = true;

        String nativeSql = this.processEscapeCodesForPrepStmts.getValue() ? nativeSQL(sql) : sql;

        if (this.useServerPrepStmts.getValue() && this.emulateUnsupportedPstmts.getValue()) {
            canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
        }
        // 如果开启了Jdbc参数useServerPrepStmts设置为true,并且canServerPrepare为true,则创建的PreparedStatement实际类型为ServerPreparedStatement
        if (this.useServerPrepStmts.getValue() && canServerPrepare) {
            if (this.cachePrepStmts.getValue()) {
                synchronized (this.serverSideStatementCache) {
                    pStmt = this.serverSideStatementCache.remove(new CompoundCacheKey(this.database, sql));

                    if (pStmt != null) {
                        ((com.mysql.cj.jdbc.ServerPreparedStatement) pStmt).setClosed(false);
                        pStmt.clearParameters();
                    }

                    if (pStmt == null) {
                        try {
                            pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType,
                                    resultSetConcurrency);
                            if (sql.length() < this.prepStmtCacheSqlLimit.getValue()) {
                                ((com.mysql.cj.jdbc.ServerPreparedStatement) pStmt).isCacheable = true;
                            }

                            pStmt.setResultSetType(resultSetType);
                            pStmt.setResultSetConcurrency(resultSetConcurrency);
                        } catch (SQLException sqlEx) {
                            // Punt, if necessary
                            if (this.emulateUnsupportedPstmts.getValue()) {
                                pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);

                                if (sql.length() < this.prepStmtCacheSqlLimit.getValue()) {
                                    this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);
                                }
                            } else {
                                throw sqlEx;
                            }
                        }
                    }
                }
            } else {
                try {
                    pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);

                    pStmt.setResultSetType(resultSetType);
                    pStmt.setResultSetConcurrency(resultSetConcurrency);
                } catch (SQLException sqlEx) {
                    // Punt, if necessary
                    if (this.emulateUnsupportedPstmts.getValue()) {
                        pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
                    } else {
                        throw sqlEx;
                    }
                }
            }
        } else {
            // 如果未开启ClientPreparedStatement,则创建的PreparedStatement实际类型为clientPrepareStatement
            pStmt = (ClientPreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
        }

        return pStmt;
    }
}

由于我们这里没有开启useServerPrepStmts,所以创建的PreparedStatement实际类型为ClientPreparedStatement。

ServerPreparedStatement具有服务端预编译的功能,也就是说同一段sql多次执行,第一次执行需要再Mysql服务端预编译sql,并返回一个id给客户端,之后客户端跟服务端的sql交互都可以使用这个id来进行,不用每次都编译sql了。除了第一次执行需要预编译会多耗费一些资源外,之后的sql执行都是比较快的。

而ClientPreparedStatement则不具备预编译的功能,每次执行sql都会讲sql信息发送到Mysql服务端处理。

如果要使用ServerPreparedStatement,需要在jdbc connection连接的url中添加useServerPrepStmts=true参数。

同时由于服务端返回的预编译id是存储在ServerPreparedStatement对象中的,所以一旦ServerPreparedStatement,这个预编译的结果就不生效了,所以Jdbc同样提供了ServerPreparedStatement的缓存功能,只需要在jdbc connection连接的url中添加cachePrepStmts=true参数,就能实现对ServerPreparedStatement的客户端缓存,该缓存是connection级别的,所以开启了该参数后,一个connection内,关闭了ServerPreparedStatement,也能使用之前预编译的结果。

2.2.3 为PreparedStatement设置运行时参数

Statement创建成功后,我们来看一下如何为PreparedStatement设置运行时参数。这里设置运行时参数,也就是把我们的实际传参设置到Statement中。之前我们使用Jdbc设置运行时参数的方式如下:

PreparedStatement ptmt = conn.prepareStatement(sql);
ptmt.setLong(1, id);

接下来我们来看一下Mybatis中,如何为PreparedStatement设置运行时参数的,实现在StatementHandler的parameterize方法中,如下:

// PreparedStatementHandler
public void parameterize(Statement statement) throws SQLException {
  parameterHandler.setParameters((PreparedStatement) statement);
}

这里的parameterHandler,是我们在RoutingStatementHandler构造函数中创建delegate(PreparedStatementHandler)时,调用PreparedStatementHandler的构造函数中创建的,类型为DefaultParameterHandler。DefaultParameterHandler中setParameters方法定义如下:

public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // 从BoundSql中获取ParameterMapping列表,每个ParameterMapping与原始SQL中的#{xxx}占位符一一对应
  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 params
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          // 为用户传入的参数parameterObject创建元信息对象
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          // 从用户传入的参数中获取propertyName对应的值
          value = metaObject.getValue(propertyName);
        }
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
        }
        try {
          //由类型处理器typeHandler向ParameterHandler设置参数
          // 比如类型为Long,则typeHandler类型为LongTypeHandler
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException | SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}
// LongTypeHandler#setParameter 继承自BaseTypeHandler
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
  if (parameter == null) {
    if (jdbcType == null) {
      throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
    }
    try {
      ps.setNull(i, jdbcType.TYPE_CODE);
    } catch (SQLException e) {
      throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
            + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
            + "Cause: " + e, e);
    }
  } else {
    try {
      // 参数非null时,设置Statement参数
      setNonNullParameter(ps, i, parameter, jdbcType);
    } catch (Exception e) {
      throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
            + "Try setting a different JdbcType for this parameter or a different configuration property. "
            + "Cause: " + e, e);
    }
  }
}

// LongTypeHandler#setNonNullParameter
public void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType)
    throws SQLException {
  ps.setLong(i, parameter);
}
// ClientPreparedStatement
public void setLong(int parameterIndex, long x) throws SQLException {
    synchronized (checkClosed().getConnectionMutex()) {
        // 向要发送给Mysql服务端的数据包Query对象的queryBindings设置参数
        ((PreparedQuery<?>) this.query).getQueryBindings().setLong(getCoreParameterIndex(parameterIndex), x);
    }
}

到这里就完成了对Statement设置运行时参数的逻辑,其实Statement设置参数,其实是将参数值设置到Statement内部成员变量query中,也就是即将要发送给Mysql服务端的数据包中必须包含的一部分数据。

2.3 通过StatementHandler执行sql查询逻辑

接下来我们来看一下如何通过StatementHandler执行sql查询,也就是通过StatementHandler的query方法。这里的StatementHandler还是之前介绍的RoutingStatementHandler,内部的装饰对象是PreparedStatementHandler,那么query的实际实现在PreparedStatementHandler中。

// PreparedStatementHandler
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  // 获取Statement,这里类型是ClientPreparedStatement
  PreparedStatement ps = (PreparedStatement) statement;
  // 调用Statement的execute方法,执行sql,结果会写回到Statement
  ps.execute();
  // 返回值映射
  return resultSetHandler.handleResultSets(ps);
}
public boolean execute() throws SQLException {
    synchronized (checkClosed().getConnectionMutex()) {

        JdbcConnection locallyScopedConn = this.connection;

        if (!this.doPingInstead && !checkReadOnlySafeStatement()) {
            throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"),
                    MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, this.exceptionInterceptor);
        }

        // 创建返回值结果对象,类型为ResultSetInternalMethods,继承了ResultSet类
        ResultSetInternalMethods rs = null;

        this.lastQueryIsOnDupKeyUpdate = false;

        if (this.retrieveGeneratedKeys) {
            this.lastQueryIsOnDupKeyUpdate = containsOnDuplicateKeyUpdateInSQL();
        }

        this.batchedGeneratedKeys = null;

        resetCancelledState();

        implicitlyCloseAllOpenResults();

        clearWarnings();

        if (this.doPingInstead) {
            doPingInstead();

            return true;
        }

        setupStreamingTimeout(locallyScopedConn);

        // 将query的带有占位符的sql和queryBinqueryBindings中的参数拼接到一起,组成完整的sql,即sendPacket
        Message sendPacket = ((PreparedQuery<?>) this.query).fillSendPacket();

        String oldDb = null;

        if (!locallyScopedConn.getDatabase().equals(this.getCurrentDatabase())) {
            oldDb = locallyScopedConn.getDatabase();
            locallyScopedConn.setDatabase(this.getCurrentDatabase());
        }

        //
        // Check if we have cached metadata for this query...
        //
        CachedResultSetMetaData cachedMetadata = null;

        boolean cacheResultSetMetadata = locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue();
        if (cacheResultSetMetadata) {
            cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery<?>) this.query).getOriginalSql());
        }

        //
        // Only apply max_rows to selects
        //
        locallyScopedConn.setSessionMaxRows(((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar() == 'S' ? this.maxRows : -1);

        // 将请求发送给Mysql服务端,获取结果ResultSet,这里类型为ResultSetInternalMethods
        rs = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(),
                (((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar() == 'S'), cachedMetadata, false);

        if (cachedMetadata != null) {
            locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery<?>) this.query).getOriginalSql(), cachedMetadata, rs);
        } else {
            if (rs.hasRows() && cacheResultSetMetadata) {
                locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery<?>) this.query).getOriginalSql(), null /* will be created */, rs);
            }
        }

        if (this.retrieveGeneratedKeys) {
            rs.setFirstCharOfQuery(((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar());
        }

        if (oldDb != null) {
            locallyScopedConn.setDatabase(oldDb);
        }

        if (rs != null) {
            this.lastInsertId = rs.getUpdateID();
            // 将查询结果ResultSet设置到ClientPreparedStatement的results成员中
            this.results = rs;
        }

        return ((rs != null) && rs.hasRows());
    }
}
protected <M extends Message> ResultSetInternalMethods executeInternal(int maxRowsToRetrieve, M sendPacket, boolean createStreamingResultSet,
        boolean queryIsSelectOnly, ColumnDefinition metadata, boolean isBatch) throws SQLException {
    synchronized (checkClosed().getConnectionMutex()) {
        try {

            JdbcConnection locallyScopedConnection = this.connection;

            ((PreparedQuery<?>) this.query).getQueryBindings()
                    .setNumberOfExecutions(((PreparedQuery<?>) this.query).getQueryBindings().getNumberOfExecutions() + 1);

            ResultSetInternalMethods rs;

            CancelQueryTask timeoutTask = null;

            try {
                timeoutTask = startQueryTimer(this, getTimeoutInMillis());

                if (!isBatch) {
                    statementBegins();
                }

                // 通过Connection连接对象向Mysql服务端发送查询请求,获取请求结果ResultSet对象
                rs = ((NativeSession) locallyScopedConnection.getSession()).execSQL(this, null, maxRowsToRetrieve, (NativePacketPayload) sendPacket,
                        createStreamingResultSet, getResultSetFactory(), metadata, isBatch);

                if (timeoutTask != null) {
                    stopQueryTimer(timeoutTask, true, true);
                    timeoutTask = null;
                }

            } finally {
                if (!isBatch) {
                    this.query.getStatementExecuting().set(false);
                }

                stopQueryTimer(timeoutTask, false, false);
            }

            return rs;
        } catch (NullPointerException npe) {
            checkClosed(); // we can't synchronize ourselves against async connection-close due to deadlock issues, so this is the next best thing for
                          // this particular corner case.

            throw npe;
        }
    }
}

到这我们就通过JDBC的Statement完成了数据库查询操作,并获取到了查询结果ResultSet。所以我们可以明确,Mybatis底层就是JDBC。此外,需要注意的是,这里获取到的结果其实是ResultSet对象,并不是我们定义的返回值对象。之前的介绍,我们也知道Mybatis会帮我们做ResultSet到自定义对象的映射。而映射的实现就在PreparedStatementHandler的query方法的最后:

resultSetHandler.handleResultSets(ps);

PreparedStatementHandler的resultSetHandler成员是在构造函数中实例化的,类型为DefaultResultSetHandler。

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;
  // Statement的getResultSet方法获取ResultSet,并将ResultSet包装为ResultSetWrapper,
  // ResultSetWrapper除了包含了ResultSet之外,还定义了数据库返回的每条数据的每行列名、列对应的JDBC类型、列对应的Java Class的类型,除此之外最主要的是还包含了TypeHandlerRegister
  ResultSetWrapper rsw = getFirstResultSet(stmt);

  // 获取MappedStatement的ResultMap,即<select>标签中定义的ResultMap,只有一个元素,但是在MappedStatement中表示是一个List
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  // resultMapCount校验,即如果select出来有结果返回,但resultMapCount为0(当前MappedStatement对应的方法找不到ResultMap,即不知道如何映射结果),抛出异常
  validateResultMapsCount(rsw, resultMapCount);

  // 将ResultSetWrapper中的值根据ResultMap,转成Java对象,先存储在multipleResults中
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  // 处理<select>中定义的resultSets(适用于多结果集),我们代码里没有使用过
  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++;
    }
  }

  // 处理multipleResults,根据其size大小,如果size=1,获取0号元素,强转为List<Object>;如果size!=1,直接返回multipleResults
  return collapseSingleResultList(multipleResults);
}

其中上面代码中提到的ResultSetWrapper如下:

更细致的如何将ResultSet解析为我们指定的ResultMap对象的,就不更详细的介绍了(比较复杂,比如嵌套结果映射,嵌套select查询,延迟查询等),大致就是反射调用ResultMap对应类的构造函数实例化对象,然后将ResultSet中的值设置到我们实例化的对象中。对于延迟查询的情况比较特殊,会为返回值实例对象创建一个代理对象,在代理中实现延迟查询的逻辑。

到这里我们就完成了Mybatis通过SqlSession完成查询的介绍,我们需要强化对如下几个概念的理解,包括Executor执行器,Statement,StatementHandler,弄明白这几个类的机制后,基本就可以弄明白Mybatis是如何实现查询逻辑的了。另外,本篇文章中,我们对DataSource和数据库连接的介绍不是很多,这其中包含一项Mybatis比较重要的优化点——连接池,我们在后面的文章再单独介绍。

参考链接:

1. Mybatis源码

2. Mybatis官方文档——动态 SQL

3. Mybatis 同时传入多个参数和对象

4. Mybatis的一级缓存和二级缓存的理解以及用法

5. jdbc中预编译语句PreparedStatement的深层分析

6. 【MyBatis源码分析】select源码分析及小结

7. Mybaits 源码解析 (八)—– 结果集 ResultSet 自动映射成实体类对象

赞(1) 打赏
Zhuoli's Blog » Mybatis源码解读『select查询——使用SqlSession』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址