上文我们已经学习到查询SQL语句的执行过程中如何获取 BoundSql!接下来继续从查询SQL语句的执行过程中如何创建 StatementHandler!喜欢的朋友们可以来个一键三连哦~
目录
查询SQL语句的执行过程
2.3 创建 StatementHandler
在 MyBatis 的源码中,StatementHandler 是一个非常核心接口。之所以说它核心,是因
为从代码分层的角度来说,StatementHandler 是 MyBatis 源码的边界,再往下层就是 JDBC 层面的接口了。StatementHandler 需要和 JDBC 层面的接口打交道,它要做的事情有很多。在执行 SQL 之前,StatementHandler 需要创建合适的 Statement 对象,然后填充参数值到
Statement 对象中,最后通过 Statement 对象执行 SQL。这还不算完,待 SQL 执行完毕,还要去处理查询结果等。这些过程看似简单,但实现起来却很复杂。好在,这些过程对应的逻辑并不需要我们亲自实现。好了,其他的就不多说了。下面我们来看一下 StatementHandler 的继承体系。
上图中,最下层的三种 StatementHandler 实现类与三种不同的 Statement 进行交互,这
个不难看出来。但 RoutingStatementHandler 则是一个奇怪的存在,因为 JDBC 中并不存在
RoutingStatement。那它有什么用呢?接下来,我们到代码中寻找答案。
// -☆- Configuration
public StatementHandler newStatementHandler(Executor executor,
MappedStatement mappedStatement,Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 创建具有路由功能的 StatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(
executor, mappedStatement, parameterObject, rowBounds,
resultHandler, boundSql);
// 应用插件到 StatementHandler 上
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler; }
如上,newStatementHandler 方法在创建 StatementHandler 之后,还会应用插件到
StatementHandler 上。关于 MyBatis 的插件机制,后面独立成章进行讲解,这里就不分析了。下面分析 RoutingStatementHandler 的代码。
public class RoutingStatementHandler implements StatementHandler {
private final StatementHandler delegate;
public RoutingStatementHandler(Executor executor, MappedStatement ms,
Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) {
// 根据 StatementType 创建不同的 StatementHandler
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("……");
}
}
// 其他方法逻辑均由别的 StatementHandler 代理完成,就不贴代码了
}
RoutingStatementHandler 的构造方法会根据 MappedStatement 中的 statementType 变量创建不同的 StatementHandler 实现类。默认情况下,statementType 值为 PREPARED。关于StatementHandler 创建的过程就先分析到这,StatementHandler 创建完成了,后续要做到事情是创建 Statement,以及将运行时参数和 Statement 进行绑定。
2.4 设置运⾏时参数到 SQL 中
JDBC 提供了三种 Statement 接口,分别是 Statement 、 PreparedStatement 和
CallableStatement。他们的关系如下:
上面三个接口的层级分明,其中 Statement 接口提供了执行 SQL,获取执行结果等基本
功能。PreparedStatement 在此基础上,对 IN 类型的参数提供了支持。使得我们可以使用运
行时参数替换 SQL 中的问号?占位符,而不用手动拼接 SQL。CallableStatement 则是在
PreparedStatement 基础上,对 OUT 类型的参数提供了支持,该种类型的参数用于保存存储
过程输出的结果。本节将分析 PreparedStatement 的创建,以及设置运行时参数到 SQL 中的过程。其他两种 Statement 的处理过程,大家请自行分析。Statement 的创建入口是在
SimpleExecutor 的 prepareStatement 方法中,下面从这个方法开始进行分析。
// -☆- SimpleExecutor
private Statement prepareStatement(StatementHandler handler, Log
statementLog) throws SQLException {
Statement stmt;
// 获取数据库连接
Connection connection = getConnection(statementLog);
// 创建 Statement,
stmt = handler.prepare(connection, transaction.getTimeout());
// 为 Statement 设置 IN 参数
handler.parameterize(stmt);
return stmt; }
上面代码的逻辑比较简单,总共包含三个步骤。如下:
- 获取数据库连接
- 创建 Statement
- 为 Statement 设置 IN 参数
上面三个步骤看起来并不难实现,实际上如果大家愿意写的话,也能写出来。不过
MyBatis 对这三个步骤进行了一些拓展,实现上也相对复杂一些。以获取数据库连接为例,
MyBatis 并未没有在 getConnection 方法中直接调用 JDBC DriverManager 的 getConnection 方法获取获取连接,而是通过数据源获取连接。MyBatis 提供了两种基于 JDBC 接口的数据源,分别为 PooledDataSource 和 UnpooledDataSource。创建或获取数据库连接的操作最终是由这两个数据源执行。本节不会分析以上两种数据源的源码,相关分析会在下一章中展开。
接下来,我将分析 PreparedStatement 的创建,以及 IN 参数设置的过程。按照顺序,先
来分析 PreparedStatement 的创建过程。如下:
// -☆- PreparedStatementHandler
public Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException {
Statement statement = null;
try {
// 创建 Statement
statement = instantiateStatement(connection);
// 设置超时和 FetchSize
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("……");
} }
protected Statement instantiateStatement(Connection connection)
throws SQLException {
String sql = boundSql.getSql();
// 根据条件调用不同的 prepareStatement 方法创建 PreparedStatement
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
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() != null) {
return connection.prepareStatement(sql,
mappedStatement.getResultSetType().getValue(),
ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareStatement(sql);
} }
PreparedStatement 的创建过程没什么复杂的地方,就不多说了。下面分析运行时参数
是如何被设置到 SQL 中的过程。
// -☆- PreparedStatementHandler
public void parameterize(Statement statement) throws SQLException {
// 通过参数处理器 ParameterHandler 设置运行时参数到 PreparedStatement 中
parameterHandler.setParameters((PreparedStatement) statement);
}
public class DefaultParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;
public void setParameters(PreparedStatement ps) {
// 从 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);
// 检测参数类型,排除掉 mode 为 OUT 类型的 parameterMapping
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
// 获取属性名
String propertyName = parameterMapping.getProperty();
// 检测 BoundSql 的 additionalParameters 是否包含 propertyName
if (boundSql.hasAdditionalParameter(propertyName)) {
value=boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
// 检测运行时参数是否有相应的类型解析器
} else if (typeHandlerRegistry.hasTypeHandler(
parameterObject.getClass())) {
// 若运行时参数的类型有相应的类型处理器 TypeHandler,则将
// parameterObject 设为当前属性的值。
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 = JdbcType.OTHER
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 由类型处理器 typeHandler 向 ParameterHandler 设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException(...);
} catch (SQLException e) {
throw new TypeException(...);
}
}
}
}
} }
如上代码,分割线以上的大段代码用于获取#{xxx}占位符属性所对应的运行时参数。分
割线以下的代码则是获取#{xxx}占位符属性对应的 TypeHandler,并在最后通过 TypeHandler将运行时参数值设置到 PreparedStatement 中。
2.5 #{}占位符的解析与参数的设置过程梳理
前面两节的内容比较多,本节将对前两节的部分内容进行梳理,以便大家能够更好理解
这两节内容之间的联系。假设我们有这样一条 SQL 语句:
SELECT * FROM author WHERE name = #{name} AND age = #{age}
这个 SQL 语句中包含两个#{}占位符,在运行时这两个占位符会被解析成两个
ParameterMapping 对象。如下:
ParameterMapping{
property='name', mode=IN,
javaType=class java.lang.String, jdbcType=null, ...}
和
ParameterMapping{
property='age', mode=IN,
javaType=class java.lang.Integer, jdbcType=null, ...}
SELECT * FROM Author WHERE name = ? AND age = ?
这里假设下面这个方法与上面的 SQL 对应:
Author findByNameAndAge(@Param("name")String name, @Param("age")Integer
age)
该方法的参数列表会被 ParamNameResolver 解析成一个 map,如下:
{
0: "name", 1: "age"
}
假设该方法在运行时有如下的调用:
findByNameAndAge("tianxiaobo", 20)
此时,需要再次借助 ParamNameResolver 的力量。这次我们将参数名和运行时的参数
值绑定起来,得到如下的映射关系。
{
"name": "tianxiaobo",
"age": 20,
"param1": "tianxiaobo",
"param2": 20
}
下一步,我们要将运行时参数设置到 SQL 中。由于原 SQL 经过解析后,占位符信息已
经被擦除掉了,我们无法直接将运行时参数 SQL 中。不过好在,这些占位符信息被记录在
了 ParameterMapping 中了,MyBatis 会将 ParameterMapping 会按照#{}占位符的解析顺序存入到 List 中。这样我们通过 ParameterMapping 在列表中的位置确定它与 SQL 中的哪一个个?占位符相关联。同时通过 ParameterMapping 中的 property 字段,我们可以到“参数名与参数值”映射表中查找具体的参数值。这样,我们就可以将参数值准确的设置到 SQL 中了,此时SQL 如下:
SELECT * FROM Author WHERE name = "tianxiaobo" AND age = 20
整个流程如下图所示。
当运行时参数被设置到 SQL 中后,下一步要做的事情是执行 SQL,然后处理 SQL 执行
结果。对于更新操作,数据库一般返回一个 int 行数值,表示受影响行数,这个处理起来比
较简单。但对于查询操作,返回的结果类型多变,处理方式也很复杂。接下来,我们就来看
看 MyBatis 是如何处理查询结果的。
2.6 处理查询结果
MyBatis 可以将查询结果,即结果集 ResultSet 自动映射成实体类对象。这样使用者就无
需再手动操作结果集,并将数据填充到实体类对象中。这可大大降低开发的工作量,提高工
作效率。在 MyBatis 中,结果集的处理工作由结果集处理器 ResultSetHandler 执行。
ResultSetHandler 是一个接口,它只有一个实现类 DefaultResultSetHandler。结果集的处理入口方法是 handleResultSets,下面来看一下该方法的实现。
public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
// 获取第一个结果集
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, res