上篇文章我们介绍了使用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源码