上文我们已经学习到查询SQL语句的执行过程中如何获取 BoundSql!接下来继续从查询SQL语句的执行过程中如何创建 StatementHandler!喜欢的朋友们可以来个一键三连哦~

目录

  • 查询SQL语句的执行过程
    • 2.3 创建 StatementHandler
    • 2.4 设置运⾏时参数到 SQL 中
    • 2.5 #{}占位符的解析与参数的设置过程梳理
    • 2.6 处理查询结果
      • 1.创建实体类对象
      • 2.结果集映射
      • 3.关联查询与延迟加载
      • 4.存储映射结果

查询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; }

上面代码的逻辑比较简单,总共包含三个步骤。如下:

  1. 获取数据库连接
  2. 创建 Statement
  3. 为 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, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {ResultMap resultMap = resultMaps.get(resultSetCount);
// 处理结果集
handleResultSet(rsw, resultMap, multipleResults, null);
// 获取下一个结果集
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;}
// 以下逻辑均与多结果集有关,就不分析了,代码省略
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {...}
return collapseSingleResultList(multipleResults);
}
private ResultSetWrapper getFirstResultSet(Statement stmt)
throws SQLException {// 获取结果集
ResultSet rs = stmt.getResultSet();
while (rs == null) {/*
* 移动 ResultSet 指针到下一个上,有些数据库驱动可能需要使用者
* 先调用 getMoreResults 方法,然后才能调用 getResultSet 方法
* 获取到第一个 ResultSet
*/
if (stmt.getMoreResults()) {rs = stmt.getResultSet();} else {if (stmt.getUpdateCount() == -1) {break;}}}
/*
* 这里并不直接返回 ResultSet,而是将其封装到 ResultSetWrapper 中。
* ResultSetWrapper 中包含了 ResultSet 一些元信息,比如列名称、
* 每列对应的 JdbcType、以及每列对应的 Java 类名(class name,譬如
* java.lang.String)等。
*/
return rs != null ? new ResultSetWrapper(rs, configuration) : null; }

如上,该方法首先从 Statement 中获取第一个结果集,然后调用 handleResultSet 方法对
该结果集进行处理。一般情况下,如果我们不调用存储过程,不会涉及到多结果集的问题。
由于存储过程并不是很常用,所以关于多结果集的处理逻辑我就不分析了。下面,我们把目
光聚焦在单结果集的处理逻辑上。

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap,
List<Object> multipleResults, ResultMapping parentMapping)
throws SQLException {try {if (parentMapping != null) {// 多结果集相关逻辑,不分析了
handleRowValues(rsw, resultMap,
null, RowBounds.DEFAULT, parentMapping);} else {/*
* 检测 resultHandler 是否为空。ResultHandler 是一个接口,使用者可
* 实现该接口,这样我们可以通过 ResultHandler 自定义接收查询结果的
* 动作。比如我们可将结果存储到 List、Map 亦或是 Set,甚至丢弃,
* 这完全取决于大家的实现逻辑。
*/
if (resultHandler == null) {// 创建默认的结果处理器
DefaultResultHandler defaultResultHandler =
new DefaultResultHandler(objectFactory);
// 处理结果集的行数据
handleRowValues(rsw, resultMap,
defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());} else {// 处理结果集的行数据
handleRowValues(rsw,resultMap,resultHandler,rowBounds,null);}}} finally {closeResultSet(rsw.getResultSet());} }

在上面代码中,出镜率最高的 handleRowValues 方法,该方法用于处理结果集中的数
据。下面来看一下这个方法的逻辑。

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap,
ResultHandler<?> resultHandler,RowBounds rowBounds,
ResultMapping parentMapping) throws SQLException {if (resultMap.hasNestedResultMaps()) {ensureNoRowBounds();
checkResultHandler();
// 处理嵌套映射,关于嵌套映射本文就不分析了
handleRowValuesForNestedResultMap(rsw,
resultMap, resultHandler, rowBounds, parentMapping);} else {// 处理简单映射
handleRowValuesForSimpleResultMap(rsw,
resultMap, resultHandler, rowBounds, parentMapping);} }

handleRowValues 方法中针对两种映射方式进行了处理。一种是嵌套映射,另一种是简
单映射。本文所说的嵌套查询是指中嵌套了一个,关于此种映射的
处理方式本节就不进行分析了。下面我将详细分析简单映射的处理逻辑,如下:

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw,
ResultMap resultMap, ResultHandler<?> resultHandler,RowBounds rowBounds,
ResultMapping parentMapping) throws SQLException {DefaultResultContext<Object> resultContext =
new DefaultResultContext<Object>();
// 根据 RowBounds 定位到指定行记录
skipRows(rsw.getResultSet(), rowBounds);
// 检测是否还有更多行的数据需要处理
while (shouldProcessMoreRows(resultContext, rowBounds) &&
rsw.getResultSet().next()) {// 获取经过鉴别器处理后的 ResultMap
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(
rsw.getResultSet(), resultMap, null);
// 从 resultSet 中获取结果
Object rowValue = getRowValue(rsw, discriminatedResultMap);
// 存储结果
storeObject(resultHandler, resultContext,
rowValue, parentMapping, rsw.getResultSet());} }

上面方法的逻辑较多,这里简单总结一下。如下:

  1. 根据 RowBounds 定位到指定行记录
  2. 循环处理多行数据
  3. 使用鉴别器处理 ResultMap
  4. 映射 ResultSet,得到映射结果 rowValue
  5. 存储结果

在如上几个步骤中,鉴别器相关的逻辑就不分析了,不是很常用。第 2 步的检测逻辑
比较简单,也忽略了。下面分析第一个步骤对应的代码逻辑。如下:

private void skipRows(ResultSet rs, RowBounds rowBounds)
throws SQLException {// 检测 rs 的类型,不同的类型行数据定位方式是不同的
if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {// 直接定位到 rowBounds.getOffset() 位置处
rs.absolute(rowBounds.getOffset());}} else {for (int i = 0; i < rowBounds.getOffset(); i++) {/*
* 通过多次调用 rs.next() 方法实现行数据定位。
* 当 Offset 数值很大时,这种效率很低下
*/
rs.next();}} }

MyBatis 默认提供了 RowBounds 用于分页,从上面的代码中可以看出,这并非是一个高
效的分页方式。除了使用 RowBounds,还可以使用一些第三方分页插件进行分页。关于第三方的分页插件,大家请自行查阅资料,这里就不展开说明了。下面分析一下 ResultSet 的映射过程,如下:

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap)
throws SQLException {final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 创建实体类对象,比如 Article 对象
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
if (rowValue != null &&
!hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
// 检测是否应该自动映射结果集
if (shouldApplyAutomaticMappings(resultMap, false)) {// 进行自动映射
foundValues = applyAutomaticMappings(
rsw, resultMap, metaObject, null) || foundValues;}
// 根据 <resultMap> 节点中配置的映射关系进行映射
foundValues = applyPropertyMappings(
rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue=foundValues || configuration.isReturnInstanceForEmptyRow()
? rowValue : null;}
return rowValue; }

上面的方法中的重要逻辑已经注释出来了,这里再简单总结一下。如下:

  1. 创建实体类对象
  2. 检测结果集是否需要自动映射,若需要则进行自动映射
  3. 按中配置的映射关系进行映射
    这三处代码的逻辑比较复杂,接下来按顺序进行分节说明。首先分析实体类的创建过程。

1.创建实体类对象

在我们的印象里,创建实体类对象是一个很简单的过程。直接通过 new 关键字,或通过
反射即可完成任务。大家可能会想,把这么简单过程也拿出来说说,怕是有凑字数的嫌疑。
实则不然,MyBatis 的维护者写了不少逻辑,以保证能成功创建实体类对象。如果实在无法
创建,则抛出异常。下面我们来看一下 MyBatis 创建实体类对象的过程。

// -☆- DefaultResultSetHandler
private Object createResultObject(ResultSetWrapper rsw,
ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {this.useConstructorMappings = false;
final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
final List<Object> constructorArgs = new ArrayList<Object>();
// 调用重载方法创建实体类对象
Object resultObject = createResultObject(rsw,
resultMap, constructorArgTypes, constructorArgs, columnPrefix);
// 检测实体类是否有相应的类型处理器
if (resultObject != null &&
!hasTypeHandlerForResultObject(rsw, resultMap.getType())) {final List<ResultMapping> propertyMappings =
resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {// 如果开启了延迟加载,则为 resultObject 生成代理类
if (propertyMapping.getNestedQueryId() != null &&
propertyMapping.isLazy()) {// 创建代理类,默认使用 Javassist 框架生成代理类。由于实体类通常
// 不会实现接口,所以不能使用 JDK 动态代理 API 为实体类生成代理。
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration,
objectFactory,constructorArgTypes, constructorArgs);
break;}}}
this.useConstructorMappings =
resultObject != null && !constructorArgTypes.isEmpty();
return resultObject; }

创建实体类对象的逻辑被封装在了 createResultObject 的重载方法中,关于该方法,待
会再分析。创建好实体类对后,还需要对中配置的映射信息进行检测。若发现
有关联查询,且关联查询结果的加载方式为延迟加载,此时需为实体类生成代理类。举个
例子说明一下,假设有如下两个实体类:

/** 作者类 */
public class Author {private Integer id;
private String name;
private Integer age;
private Integer sex; }
/** 文章类 */
public class Article {private Integer id;
private String title;
// 一对一关系
private Author author;
private String content; }

如上,Article 对象中的数据由一条 SQL 从 article 表中查询。Article 类有一个 author 字
段,该字段的数据由另一条 SQL 从 author 表中查出。我们在将 article 表的查询结果填充到
Article 类对象中时,并不希望 MyBaits 立即执行另一条 SQL 查询 author 字段对应的数据。
而是期望在我们调用 article.getAuthor()方法时,MyBaits 再执行另一条 SQL 从 author 表中查询出所需的数据。若如此,我们需要改造 getAuthor 方法,以保证调用该方法时可让 MyBaits执行相关的 SQL。关于延迟加载后面将会进行详细的分析,这里先说这么多。下面分析createResultObject 重载方法的逻辑,如下:

private Object createResultObject(ResultSetWrapper rsw, ResultMap
resultMap, List<Class<?>> constructorArgTypes, List<Object>
constructorArgs, String columnPrefix) throws SQLException {final Class<?> resultType = resultMap.getType();
final MetaClass metaType =
MetaClass.forClass(resultType, reflectorFactory);
// 获取 <constructor> 节点对应的 ResultMapping
final List<ResultMapping> constructorMappings =
resultMap.getConstructorResultMappings();
// 检测是否有与返回值类型相对应的 TypeHandler,若有则直接从
// 通过 TypeHandler 从结果集中ᨀ取数据,并生成返回值对象
if (hasTypeHandlerForResultObject(rsw, resultType)) {// 通过 TypeHandler 获取ᨀ取,并生成返回值对象
return createPrimitiveResultObject(rsw, resultMap, columnPrefix);} else if (!constructorMappings.isEmpty()) {// 通过 <constructor> 节点配置的映射信息从 ResultSet 中ᨀ取数据,
// 然后将这些数据传给指定构造方法,即可创建实体类对象
return createParameterizedResultObject(rsw, resultType,
constructorMappings, constructorArgTypes,
constructorArgs, columnPrefix);} else if(resultType.isInterface() || metaType.hasDefaultConstructor()){// 通过 ObjectFactory 调用目标类的默认构造方法创建实例
return objectFactory.create(resultType);} else if (shouldApplyAutomaticMappings(resultMap, false)) {// 通过自动映射查找合适的构造方法创建实例
return createByConstructorSignature(rsw, resultType,
constructorArgTypes, constructorArgs, columnPrefix);}
throw new ExecutorException("……");
}

createResultObject 方法中包含了 4 种创建实体类对象的方式。一般情况下,若无特殊要
求,MyBatis 会通过 ObjectFactory 调用默认构造方法创建实体类对象。ObjectFactory 是一个接口,大家可以实现这个接口,以按照自己的逻辑控制对象的创建过程。至此,实体类对象创建好了,接下里要做的事情是将结果集中的数据映射到实体类对象中。

2.结果集映射

在 MyBatis 中,结果集自动映射有三种等级。这三种等级官方文档上有所说明,这里直
接引用一下。如下:

  • NONE - 禁用自动映射。仅设置手动映射属性
  • PARTIAL - 将自动映射结果除了那些有内部定义内嵌结果映射的(joins)
  • FULL - 自动映射所有

除了以上三种等级,我们还可以显示配置节点的 autoMapping 属性,以启用
或者禁用指定 ResultMap 的自动映射设定。下面,来看一下自动映射相关的逻辑。

private boolean shouldApplyAutomaticMappings(
ResultMap resultMap, boolean isNested) {// 检测 <resultMap> 是否配置了 autoMapping 属性
if (resultMap.getAutoMapping() != null) {// 返回 autoMapping 属性
return resultMap.getAutoMapping();} else {if (isNested) {// 对于嵌套 resultMap,仅当全局的映射行为为 FULL 时,才进行自动映射
return AutoMappingBehavior.FULL ==
configuration.getAutoMappingBehavior();
} else {// 对于普通的 resultMap,只要全局的映射行为不为 NONE,即可进行自动映射
return AutoMappingBehavior.NONE !=
configuration.getAutoMappingBehavior();}} }

shouldApplyAutomaticMappings 方法用于检测是否应为当前结果集应用自动映射。检测
结果取决于节点的 autoMapping 属性,以及全局自动映射行为。上面代码的逻辑
不难理解,就不多说了。下面来分析 MyBatis 是如何进行自动映射的。

private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap
resultMap, MetaObject metaObject, String columnPrefix)
throws SQLException {// 获取 UnMappedColumnAutoMapping 列表
List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(
rsw, resultMap, metaObject, columnPrefix);
boolean foundValues = false;
if (!autoMapping.isEmpty()) {for (UnMappedColumnAutoMapping mapping : autoMapping) {// 通过 TypeHandler 从结果集中获取指定列的数据
final Object value = mapping.typeHandler
.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {foundValues = true;}
if (value != null || (configuration.isCallSettersOnNulls() &&
!mapping.primitive)) {// 通过元信息对象设置 value 到实体类对象的指定字段上
metaObject.setValue(mapping.property, value);
}}}
return foundValues; }

applyAutomaticMappings 方法的代码不多,逻辑也不是很复杂。首先是获取
UnMappedColumnAutoMapping 集合,然后遍历该集合,并通过 TypeHandler 从结果集中获取数据,最后再将获取到的数据设置到实体类对象中。虽然逻辑上看起来没什么复杂的东西,但如果不清楚 UnMappedColumnAutoMapping 的用途,是无法理解上面代码的逻辑的。所以这里简单介绍一下 UnMappedColumnAutoMapping 的用途。UnMappedColumnAutoMapping用于记录未配置在节点中的映射关系。该类定义在 DefaultResultSetHandler 内部,它的代码如下:

private static class UnMappedColumnAutoMapping {private final String column;
private final String property;
private final TypeHandler<?> typeHandler;
private final boolean primitive;
public UnMappedColumnAutoMapping(String column, String property,
TypeHandler<?> typeHandler, boolean primitive) {this.column = column;
this.property = property;
this.typeHandler = typeHandler;
this.primitive = primitive;} }

以上就是 UnMappedColumnAutoMapping 类的所有代码,没什么逻辑,仅用于记录映射
关系。下面看一下获取 UnMappedColumnAutoMapping 集合的过程。

// -☆- DefaultResultSetHandler
private List<UnMappedColumnAutoMapping> createAutomaticMappings(
ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
String columnPrefix) throws SQLException {final String mapKey = resultMap.getId() + ":" + columnPrefix;
// 从缓存中获取 UnMappedColumnAutoMapping 列表
List<UnMappedColumnAutoMapping> autoMapping =
autoMappingsCache.get(mapKey);
// 缓存未命中
if (autoMapping == null) {autoMapping = new ArrayList<UnMappedColumnAutoMapping>();
// 从 ResultSetWrapper 中获取未配置在 <resultMap> 中的列名
final List<String> unmappedColumnNames =
rsw.getUnmappedColumnNames(resultMap, columnPrefix);
for (String columnName : unmappedColumnNames) {String propertyName = columnName;
if (columnPrefix != null && !columnPrefix.isEmpty()) {if (columnName.toUpperCase(Locale.ENGLISH) .startsWith(columnPrefix)) {// 获取不包含列名前缀的属性名
propertyName =
columnName.substring(columnPrefix.length());} else {continue;}}
// 将下划线形式的列名转成驼峰式,比如 AUTHOR_NAME -> authorName
final String property = metaObject.findProperty(
propertyName, configuration.isMapUnderscoreToCamelCase());
if (property != null && metaObject.hasSetter(property)) {// 检测当前属性是否存在于 resultMap 中
if (resultMap.getMappedProperties().contains(property)) {continue;}// 获取属性对于的类型
final Class<?> propertyType =
metaObject.getSetterType(property);
if (typeHandlerRegistry.hasTypeHandler(
propertyType, rsw.getJdbcType(columnName))) {// 获取类型处理器
final TypeHandler<?> typeHandler =
rsw.getTypeHandler(propertyType, columnName);
// 封装上面获取到的信息到 UnMappedColumnAutoMapping 对象中
autoMapping.add(new UnMappedColumnAutoMapping(
columnName, property, typeHandler,
propertyType.isPrimitive()));} else {configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement,
columnName, property, propertyType);}} else {// 若 property 为空,或实体类中无 property 属性,此时无法完成
// 列名与实体类属性建立映射关系。针对这种情况,有三种处理方式,
// 1. 什么都不做
// 2. 仅打印日志
// 3. 抛出异常
// 默认情况下,是什么都不做
configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName,
(property != null) ? property : propertyName, null);}}
// 写入缓存
autoMappingsCache.put(mapKey, autoMapping);}
return autoMapping; }

上面的代码有点多,不过不用太担心,耐心看一下,还是可以看懂的。下面总结一下这
个方法的逻辑。

  1. 从 ResultSetWrapper 中获取未配置在中的列名
  2. 遍历上一步获取到的列名列表
  3. 若列名包含列名前缀,则移除列名前缀,得到属性名
  4. 将下划线形式的列名转成驼峰式
  5. 获取属性类型
  6. 获取类型处理器
  7. 创建 UnMappedColumnAutoMapping 实例

以上步骤中,除了第一步,其他都是常规操作,无需过多说明。下面来分析第一个步
骤的逻辑,如下:

// -☆- ResultSetWrapper
public List<String> getUnmappedColumnNames(ResultMap resultMap,
String columnPrefix) throws SQLException {List<String> unMappedColumnNames = unMappedColumnNamesMap.get(
getMapKey(resultMap, columnPrefix));
if (unMappedColumnNames == null) {// 加载已映射与未映射列名
loadMappedAndUnmappedColumnNames(resultMap, columnPrefix);
// 获取未映射列名
unMappedColumnNames = unMappedColumnNamesMap.get(
getMapKey(resultMap, columnPrefix));}
return unMappedColumnNames; }
private void loadMappedAndUnmappedColumnNames(ResultMap resultMap,
String columnPrefix) throws SQLException {List<String> mappedColumnNames = new ArrayList<String>();
List<String> unmappedColumnNames = new ArrayList<String>();
final String upperColumnPrefix = columnPrefix == null ?
null : columnPrefix.toUpperCase(Locale.ENGLISH);
// 为 <resultMap> 中的列名拼接前缀
final Set<String> mappedColumns = prependPrefixes(
resultMap.getMappedColumns(), upperColumnPrefix);
// 遍历 columnNames,columnNames 是 ResultSetWrapper 的成员变量,
// 保存了当前结果集中的所有列名
for (String columnName : columnNames) {final String upperColumnName =
columnName.toUpperCase(Locale.ENGLISH);
// 检测已映射列名集合中是否包含当前列名
if (mappedColumns.contains(upperColumnName)) {mappedColumnNames.add(upperColumnName);} else {// 将列名存入 unmappedColumnNames 中
unmappedColumnNames.add(columnName);}}
// 缓存列名集合
mappedColumnNamesMap.put(
getMapKey(resultMap, columnPrefix), mappedColumnNames);
unMappedColumnNamesMap.put(
getMapKey(resultMap, columnPrefix), unmappedColumnNames);
}

如上,已映射列名与未映射列名的分拣逻辑并不复杂,这里简述一下相关逻辑。首先是
从当前数据集中获取列名集合,然后获取中配置的列名集合。之后遍历数据集中
的列名集合,并判断列名是否被配置在了节点中。若配置了,则表明该列名已有
映射关系,此时该列名存入 mappedColumnNames 中。若未配置,则表明列名未与实体类的某个字段形成映射关系,此时该列名存入 unmappedColumnNames 中。这样,列名的分拣工作就完成了。分拣过程示意图如下:

如上图所示,实体类 Author 的 id 和 name 字段与列名 id 和 name 被配置在了<resultMap>
中,它们之间形成了映射关系。列名 age、sex 和 email 未配置在<resultMap>中,因此未与Author 中的字段形成映射,所以他们最终都被放入了 unMappedColumnNames 集合中。弄懂了未映射列名获取的过程,自动映射的代码逻辑就不难懂了。好了,关于自动映射的分析就先到这,接下来分析一下 MyBatis 是如何将结果集中的数据填充到已映射的实体类字段中的。

// -☆- DefaultResultSetHandler
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;
// 获取 ResultMapping
final List<ResultMapping> propertyMappings =
resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {// 拼接列名前缀,得到完整列名
String column = prependPrefix(
propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.getNestedResultMapId() != null) {column = null;}
/*
* 下面的 if 分支由三个或条件组合而成,三个条件的含义如下:
* 条件一:检测 column 是否为 {prop1=col1, prop2=col2} 形式,该
* 种形式的 column 一般用于关联查询
* 条件二:检测当前列名是否被包含在已映射的列名集合中,
* 若包含则可进行数据集映射操作
* 条件三:多结果集相关,暂不分析
*/
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(
column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {// 从结果集中获取指定列的数据
Object value = getPropertyMappingValue(rsw.getResultSet(),
metaObject, propertyMapping, lazyLoader, columnPrefix);
final String property = propertyMapping.getProperty();
if (property == null) {continue;
// 若获取到的值为 DEFERED,则延迟加载该值} else if (value == DEFERED) {foundValues = true;
continue;}
if (value != null) {foundValues = true;}
if (value != null || (configuration.isCallSettersOnNulls() &&
!metaObject.getSetterType(property).isPrimitive())) {// 将获取到的值设置到实体类对象中
metaObject.setValue(property, value);}}}
return foundValues; }
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);
return DEFERED;} else {final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
// 拼接前缀
final String column = prependPrefix(propertyMapping.getColumn(),
columnPrefix);
// 从 ResultSet 中获取指定列的值
return typeHandler.getResult(rs, column);} }

applyPropertyMappings 方法首先从 ResultSetWrapper 中获取已映射列名集合
mappedColumnNames, 从 ResultMap 获取映射对象 ResultMapping 集合。然后遍历
ResultMapping 集合,在此过程中调用 getPropertyMappingValue 获取指定指定列的数据,最后将获取到的数据设置到实体类对象中。到此,基本的结果集映射过程就分析完了。

3.关联查询与延迟加载

我们在学习 MyBatis 框架时,会经常碰到一对一,一对多的使用场景。对于这样的场景,
通常我们可以用一条 SQL 进行多表查询完成任务。当然我们也可以使用关联查询,将一条
SQL 拆成两条去完成查询任务。MyBatis 提供了两个标签用于支持一对一和一对多的使用场
景,分别是和。下面我来演示一下如何使用完成一对一的关联查询。先来看看实体类的定义:

/** 作者类 */
public class Author {private Integer id;
private String name;
private Integer age;
private Integer sex;
private String email;
// 省略 getter/setter
}
/** 文章类 */
public class Article {private Integer id;
private String title;
// 一对一关系
private Author author;
private String content;
private Date createTime;
// 省略 getter/setter
}

相关表记录如下

接下来看一下 Mapper 接口与映射文件的定义。

public interface ArticleDao {Article findOne(@Param("id") int id);
Author findAuthor(@Param("id") int authorId);
}
<mapper namespace="xyz.coolblog.chapter4.dao.ArticleDao">
<resultMap id="articleResult" type="Article">
<result property="createTime" column="create_time"/>
<association property="author" column="author_id"
javaType="Author" select="findAuthor"/>
</resultMap>
<select id="findOne" resultMap="articleResult">SELECTid, author_id, title, content, create_timeFROMarticleWHEREid = #{id}
</select>
<select id="findAuthor" resultType="Author">SELECTid, name, age, sex, emailFROMauthorWHEREid = #{id}
</select>
</mapper>

好了,必要在的准备工作做完了,下面可以写测试代码了。如下:

public class OneToOneTest {private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {String resource = "chapter4/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();}
@Test
public void testOne2One() {SqlSession session = sqlSessionFactory.openSession();
try {ArticleDao articleDao = session.getMapper(ArticleDao.class);
Article article = articleDao.findOne(1);
Author author = article.getAuthor();
article.setAuthor(null);
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\nauthor info:");
System.out.println(author);} finally {session.close();}} }

测试结果如下:

如上,从上面的输出结果中可以看出,我们在调用 ArticleDao 的 findOne 方法时,MyBatis
执行了两条 SQL,完成了一对一的查询需求。理解了上面的例子后,下面就可以深入到源码
中,看看 MyBatis 是如何实现关联查询的。接下里从 getNestedQueryMappingValue 方法开始分析,如下:

private Object getNestedQueryMappingValue(ResultSet rs,
MetaObject metaResultObject, ResultMapping propertyMapping,
ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {// 获取关联查询 id,id = 命名空间 + <association> 的 select 属性值
final String nestedQueryId = propertyMapping.getNestedQueryId();
final String property = propertyMapping.getProperty();
// 根据 nestedQueryId 获取 MappedStatement
final MappedStatement nestedQuery =
configuration.getMappedStatement(nestedQueryId);
final Class<?> nestedQueryParameterType =
nestedQuery.getParameterMap().getType();
/*
* 生成关联查询语句参数对象,参数类型可能是一些包装类,Map 或是自定义的实体类,
* 具体类型取决于配置信息。以上面的例子为基础,下面分析不同配置对
* 参数类型的影响:
* 1. <association column="author_id">
* column 属性值仅包含列信息,参数类型为 author_id 列对应的类型,
* 这里为 Integer
* 2. <association column="{id=author_id, name=title}">
* column 属性值包含了属性名与列名的复合信息,MyBatis 会根据列名从
* ResultSet 中获取列数据,并将列数据设置到实体类对象的指定属性中,比如:
* Author{id=1, name="MyBatis 源码分析系列文章导读", age=null, …}
* 或是以键值对 <属性, 列数据> 的形式,将两者存入 Map 中。比如:
* {"id": 1, "name": "MyBatis 源码分析系列文章导读"}
*
* 至于参数类型到底为实体类还是 Map,取决于关联查询语句的配置信息。比如:
* <select id="findAuthor"> -> 参数类型为 Map
* <select id="findAuthor" parameterType="Author">
* -> 参数类型为实体类
*/
final Object nestedQueryParameterObject=prepareParameterForNestedQuery(
rs, propertyMapping, nestedQueryParameterType, columnPrefix);
Object value = null;
if (nestedQueryParameterObject != null) {// 获取 BoundSql
final BoundSql nestedBoundSql =
nestedQuery.getBoundSql(nestedQueryParameterObject);
final CacheKey key = executor.createCacheKey(nestedQuery,
nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
final Class<?> targetType = propertyMapping.getJavaType();
// 检查一级缓存是否保存了关联查询结果
if (executor.isCached(nestedQuery, key)) {// 从一级缓存中获取关联查询的结果,并通过 metaResultObject
// 将结果设置到相应的实体类对象中
executor.deferLoad(nestedQuery,
metaResultObject, property, key, targetType);
value = DEFERED;} else {// 创建结果加载器
final ResultLoader resultLoader = new ResultLoader(
configuration, executor, nestedQuery,
nestedQueryParameterObject, targetType, key, nestedBoundSql);
// 检测当前属性是否需要延迟加载
if (propertyMapping.isLazy()) {// 添加延迟加载相关的对象到 loaderMap 集合中
lazyLoader.addLoader(
property, metaResultObject, resultLoader);
value = DEFERED;} else {// 直接执行关联查询
value = resultLoader.loadResult();}}}
return value; }

上面对关联查询进行了比较多的注释,导致该方法看起来有点复杂。当然,真实的逻辑
确实有点复杂,因为它还调用了其他的很多方法。下面先来总结一下该方法的逻辑:

  1. 根据 nestedQueryId 获取 MappedStatement
  2. 生成参数对象
  3. 获取 BoundSql
  4. 检测一级缓存中是否有关联查询的结果,若有,则将结果设置到实体类对象中
  5. 若一级缓存未命中,则创建结果加载器 ResultLoader
  6. 检测当前属性是否需要进行延迟加载,若需要,则添加延迟加载相关的对象到
    loaderMap 集合中
  7. 如不需要延迟加载,则直接通过结果加载器加载结果

如上,getNestedQueryMappingValue 方法中逻辑多是都是和延迟加载有关。除了延迟加
载,以上流程中针对一级缓存的检查是十分有必要的,若缓存命中,可直接取用结果,无需
再在执行关联查询 SQL。若缓存未命中,接下来就要按部就班执行延迟加载相关逻辑,接下
来,分析一下 MyBatis 延迟加载是如何实现的。首先我们来看一下添加延迟加载相关对象到
loaderMap 集合中的逻辑,如下:

// -☆- ResultLoaderMap
public void addLoader(String property, MetaObject metaResultObject,
ResultLoader resultLoader) {// 将属性名转为大写
String upperFirst = getUppercaseFirstProperty(property);
if (!upperFirst.equalsIgnoreCase(property) &&
loaderMap.containsKey(upperFirst)) {throw new ExecutorException("……");}
// 创建 LoadPair,并将 <大写属性名,LoadPair 对象> 键值对添加到 loaderMap 中
loaderMap.put(upperFirst,
new LoadPair(property, metaResultObject, resultLoader));
}

addLoader 方法的参数最终都传给了 LoadPair,该类的 load 方法会在内部调用
ResultLoader 的 loadResult 方法进行关联查询,并通过 metaResultObject 将查询结果设置到实
体类对象中。那 LoadPair 的 load 方法由谁调用呢?答案是实体类的代理对象。下面我们修改一下上面示例中的部分代码,演示一下延迟加载。首先,我们需要在 MyBatis 配置文件的
<settings>节点中加入或覆盖如下配置:

<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 关闭积极的加载策略 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 延迟加载的触发方法 -->
<setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>

上面三个配置 MyBatis 官方文档中有较为详细的介绍,大家可以参考官方文档,这里就
不详细介绍了。下面修改一下测试类的代码:

public class OneToOneTest {private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {...}
@Test
public void testOne2One2() {SqlSession session = sqlSessionFactory.openSession();
try {ArticleDao articleDao = session.getMapper(ArticleDao.class);
Article article = articleDao.findOne(1);
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\n 延迟加载 author 字段:");
// 通过 getter 方法触发延迟加载
Author author = article.getAuthor();
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\nauthor info:");
System.out.println(author);} finally {session.close();}} }

测试结果如下:

从上面结果中可以看出,我们在未调用 getAuthor 方法时,Article 对象中的 author 字段
为 null。调用该方法后,再次输出 Article 对象,发现其 author 字段有值了,表明 author 字段的延迟加载逻辑被触发了。既然调用 getAuthor 可以触发延迟加载,那么该方法一定被做过手脚了,不然该方法应该返回 null 才是。实际情况确实如此,MyBatis 会为需要延迟加载的类生成代理类,代理逻辑会拦截实体类的方法调用。默认情况下,MyBatis 会使用 Javassist为实体类生成代理,代理逻辑封装在 JavassistProxyFactory 类中,下面一起看一下。

// -☆- JavassistProxyFactory
public Object invoke(Object enhanced, Method method, Method methodProxy,
Object[] args) throws Throwable {final String methodName = method.getName();
try {synchronized (lazyLoader) {if (WRITE_REPLACE_METHOD.equals(methodName)) {// 针对 writeReplace 方法的处理逻辑,与延迟加载无关,不分析了} else {if (lazyLoader.size() > 0 &&
!FINALIZE_METHOD.equals(methodName)) {// 如果 aggressive 为 true,或触发方法(比如 equals,
// hashCode 等)被调用,则加载所有的所有延迟加载的数据
if (aggressive ||
lazyLoadTriggerMethods.contains(methodName)) {lazyLoader.loadAll();} else if (PropertyNamer.isSetter(methodName)) {final String property =
PropertyNamer.methodToProperty(methodName);
// 如果使用者显示调用了 setter 方法,则将相应的
// 延迟加载类从 loaderMap 中移除
lazyLoader.remove(property);
// 检测使用者是否调用 getter 方法} else if (PropertyNamer.isGetter(methodName)) {final String property =
PropertyNamer.methodToProperty(methodName);
// 检测该属性是否有相应的 LoadPair 对象
if (lazyLoader.hasLoader(property)) {// 执行延迟加载逻辑
lazyLoader.load(property);}}}}}
// 调用被代理类的方法
return methodProxy.invoke(enhanced, args);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);} }

如上,代理方法首先会检查 aggressive 是否为 true ,如果不满足,再去检查
lazyLoadTriggerMethods 是否包含当前方法名。这里两个条件只要一个为 true,当前实体类
中所有需要延迟加载。aggressive 和 lazyLoadTriggerMethods 两个变量的值取决于下面的配置。

<setting name="aggressiveLazyLoading" value="false"/>
<setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>

回到上面的代码中。如果执行线程未进入第一个条件分支,那么紧接着,代理逻辑会检
查使用者是不是调用了实体类的 setter 方法。如果调用了,就将该属性对应的 LoadPair 从
loaderMap 中移除。为什么要这么做呢?答案是:使用者既然手动调用 setter 方法,说明使用者想自定义某个属性的值。此时,延迟加载逻辑不应该再修改该属性的值,所以这里从
loaderMap 中移除属性对于的 LoadPair。最后如果使用者调用的是某个属性的 getter 方法,
且该属性配置了延迟加载,此时延迟加载逻辑就会被触发。那接下来,我们来看看延迟加载
逻辑是怎样实现的的。

// -☆- ResultLoaderMap
public boolean load(String property) throws SQLException {// 从 loaderMap 中移除 property 所对应的 LoadPair
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {// 加载结果
pair.load();
return true;}
return false; }
// -☆- LoadPair
public void load() throws SQLException {if (this.metaResultObject == null) {throw new IllegalArgumentException("metaResultObject is null");}
if (this.resultLoader == null) {throw new IllegalArgumentException("resultLoader is null");}
// 调用重载方法
this.load(null);
}
public void load(final Object userObject) throws SQLException {// 若 metaResultObject 和 resultLoader 为 null,则创建相关对象。
// 在当前调用情况下,两者均不为 null,条件不成立。篇幅原因,下面代码不分析了
if (this.metaResultObject == null || this.resultLoader == null) {...}
// 线程安全检测
if (this.serializationCheck == null) {final ResultLoader old = this.resultLoader;
// 重新创建新的 ResultLoader 和 ClosedExecutor,
// ClosedExecutor 是非线程安全的
this.resultLoader = new ResultLoader(old.configuration,
new ClosedExecutor(), old.mappedStatement, old.parameterObject,
old.targetType, old.cacheKey, old.boundSql);}
// 调用 ResultLoader 的 loadResult 方法加载结果,
// 并通过 metaResultObject 设置结果到实体类对象中
this.metaResultObject.setValue(property,this.resultLoader.loadResult());
}

上面的代码比较多,但是没什么特别的逻辑,我们重点关注最后一行有效代码就行了。
下面看一下 ResultLoader 的 loadResult 方法逻辑是怎样的。

public Object loadResult() throws SQLException {// 执行关联查询
List<Object> list = selectList();
// 抽取结果
resultObject = resultExtractor.extractObjectFromList(list, targetType);
return resultObject; }
private <E> List<E> selectList() throws SQLException {Executor localExecutor = executor;
if (Thread.currentThread().getId() != this.creatorThreadId ||
localExecutor.isClosed()) {localExecutor = newExecutor();}
try {// 通过 Executor 就行查询,这个之前已经分析过了
return localExecutor.<E>query(mappedStatement, parameterObject,
RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey,boundSql);} finally {if (localExecutor != executor) {localExecutor.close(false);}} }

如上,我们在 ResultLoader 中终于看到了执行关联查询的代码,即 selectList 方法中的
逻辑。该方法在内部通过 Executor 进行查询。至于查询结果的抽取过程,并不是本节所关心
的点,因此大家自行分析吧。到此,关于关联查询与延迟加载就分析完了。

4.存储映射结果

存储映射结果是“查询结果”处理流程中的最后一环,实际上也是查询语句执行过程的最
后一环。本节内容分析完,整个查询过程就分析完了,那接下来让我们带着喜悦的心情来分
析映射结果存储逻辑。

private void storeObject(ResultHandler<?> resultHandler,
DefaultResultContext<Object> resultContext,Object rowValue, ResultMapping
parentMapping, ResultSet rs) throws SQLException {if (parentMapping != null) {// 多结果集相关,不分析了
linkToParents(rs, parentMapping, rowValue);} else {// 存储结果
callResultHandler(resultHandler, resultContext, rowValue);} }
private void callResultHandler(ResultHandler<?> resultHandler,
DefaultResultContext<Object> resultContext, Object rowValue) {// 设置结果到 resultContext 中
resultContext.nextResultObject(rowValue);
// 从 resultContext 获取结果,并存储到 resultHandler 中((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}

上面方法显示将 rowValue 设置到 ResultContext 中,然后再将 ResultContext 对象作为参
数传给 ResultHandler 的 handleResult 方法。下面我们分别看一下 ResultContext 和
ResultHandler 的实现类。如下:

public class DefaultResultContext<T> implements ResultContext<T> {private T resultObject;
private int resultCount;
/** 状态字段 */
private boolean stopped;
// 省略部分代码
@Override
public boolean isStopped() {return stopped;}
public void nextResultObject(T resultObject) {resultCount++;
this.resultObject = resultObject;}
@Override
public void stop() {this.stopped = true;} }

DefaultResultContext 中包含了一个状态字段,表明结果上下文的状态。在处理多行数据
时,MyBatis 会检查该字段的值,已决定是否需要进行后续的处理。该类的逻辑比较简单,
不多说了。下面再来看一下 DefaultResultHandler 的源码。

public class DefaultResultHandler implements ResultHandler<Object> {private final List<Object> list;
public DefaultResultHandler() {list = new ArrayList<Object>();}@Override
public void handleResult(ResultContext<? extends Object> context) {// 添加结果到 list 中
list.add(context.getResultObject());}
public List<Object> getResultList() {return list;} }

如上,DefaultResultHandler 默认使用 List 存储结果。除此之外,如果 Mapper(或 Dao)
接口方法返回值为 Map 类型,此时则需要另一种 ResultHandler 实现类处理结果,即
DefaultMapResultHandler。关于 DefaultMapResultHandler 的源码大家自行分析吧啊,本节就不展开了。

一万一千字!结合代码超详细讲解SQL执行流程(二)!干货到底!建议收藏!相关推荐

  1. bat脚本常用命令及亲测示例代码超详细讲解

    这篇文章主要介绍了bat脚本常用命令及亲测示例代码超详细讲解,在这里需要注意编辑bat文件请使用ANSI编码,不然容易出现中文乱码,需要的朋友可以参考下 目录一 1.语句注释 2.暂停 3.输出和换行 ...

  2. 【超详细】一文学会链表解题(建议收藏!)

    简介: 如果说数据结构是算法的基础,那么数组和链表就是数据结构的基础. 因为像堆,栈,对,图等比较复杂的数组结基本上都可以由数组和链表来表示,所以掌握数组和链表的基本操作十分重要.本文将为大家讲解链表 ...

  3. 【算法笔记】一步一步推出来的同余最短路优化思路(千字长文,超详细)

    整理的算法模板合集: ACM模板 目录 同余最短路 例题1:luogu P3403 跳楼机 例题2:luogu P2371 [国家集训队]墨墨的等式 例题3:luogu P2662 牛场围栏 同余最短 ...

  4. 大数据分析-零基础学Tableau+超详细讲解+示例练习(二)

    前文回顾: 零基础学Tableau(一):条形图.直方图绘制:传送门 目录 数据前处理 Tableau绘制折线图 Tableau绘制饼图与环形图 饼图 环形图 示例 数据前处理 处理前,现在数据的样式 ...

  5. Java基础18-String类【String类的特点对象个数常用方法】【超详细讲解】

    Java基础-String类[超详细讲解] String类的特点 String在java.lang.String包中 1:特点 (1)String类型不能被继承,因为由final修饰 (2)Strin ...

  6. Python的零基础超详细讲解(第十三天)-Python的类与对象

    基础篇往期文章如下: Python的零基础超详细讲解(第一天)-Python简介以及下载 Python的零基础超详细讲解(第二天)-Python的基础语法1 Python的零基础超详细讲解(第三天)- ...

  7. Python的零基础超详细讲解(第十二天)-Python函数及使用

    基础篇往期文章: Python的零基础超详细讲解(第一天)-Python简介以及下载_编程简单学的博客-CSDN博客 Python的零基础超详细讲解(第二天)-Python的基础语法1_编程简单学的博 ...

  8. Python的零基础超详细讲解(第七天)-Python的数据的应用

    往期文章 Python的零基础超详细讲解(第一天)-Python简介以及下载_编程简单学的博客-CSDN博客 Python的零基础超详细讲解(第二天)-Python的基础语法1_编程简单学的博客-CS ...

  9. Python的零基础超详细讲解(第五天)-Python的运算符

    往期文章 Python的零基础超详细讲解(第一天)-Python简介以及下载_编程简单学的博客-CSDN博客 Python的零基础超详细讲解(第二天)-Python的基础语法1_编程简单学的博客-CS ...

最新文章

  1. Android开发之第三方推送JPush极光推送知识点详解 学会集成第三方SDK推送
  2. ireport 找不到子报表:Could not load object from location
  3. java提高篇之理解java的三大特性——多态
  4. 截取指定字符前_VBA学习笔记35-1:字符串拆分与组合
  5. 二元一次方程有唯一解的条件_人教版初中数学七年级下册用适当方法解二元一次方程组公开课优质课课件教案视频...
  6. Xftp上传文件显示状态错误
  7. 编程新手选择开发语言的注意事项
  8. Python基础闯关失败总结
  9. JavaScript 造就年薪超过 10 万美元的开发者们!
  10. 碱性干电池的内阻测试方法_电池内阻怎么测
  11. 拓端tecdat|python用遗传算法 神经网络 模糊逻辑控制算法对彩票乐透数据进行预测
  12. 什么是ECS框架?讲解 + 实战带你入门ECS框架
  13. 【零基础玩转BLDC系列】基于霍尔传感器的无刷直流电机控制原理
  14. apk编辑器找Android,教你用安卓神器APK编辑器去除程序广告
  15. 中国计算机展望未来,作文:展望未来中国的2025
  16. 用JAVA时间类计算今天到指定日期的天数统计
  17. AI 考古比胡八一更高效
  18. Postman Collection Format v1 is no longer supported and can not be imported directly. You may conver
  19. 11 计算机组成原理第七章 输入/输出系统 I/O系统基本概念 外部设备
  20. 计算机软件技术基础复习

热门文章

  1. idea中ctrl+shift+f(在文件中查找)失效,全图文解决方案
  2. vue 全局/局部组件
  3. MySQL死锁问题如何分析锁表后查看死锁和去除死锁快速解决方法
  4. Python海龟画图
  5. 自动生成小程序的智能建站系统,项目分享
  6. JavaScript学习笔记(BOM编程案例)
  7. 透过微信浏览器看开源问题
  8. 盘扣架市场持续低迷,春节后是否能“破圈”?
  9. Kali Linux渗透测试 128 拒绝服务--TearDrop 攻击
  10. python老师培训课