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

上文我们已经学习到查询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 的继承体系。
一万一千字!结合代码超详细讲解SQL执行流程(二)!干货到底!建议收藏!_第1张图片

上图中,最下层的三种 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执行流程(二)!干货到底!建议收藏!_第2张图片
当运行时参数被设置到 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;
 }
// 根据  节点中配置的映射关系进行映射
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);
// 获取  节点对应的 ResultMapping
final List<ResultMapping> constructorMappings =
resultMap.getConstructorResultMappings();
// 检测是否有与返回值类型相对应的 TypeHandler,若有则直接从
// 通过 TypeHandler 从结果集中ᨀ取数据,并生成返回值对象
if (hasTypeHandlerForResultObject(rsw, resultType)) {
     
// 通过 TypeHandler 获取ᨀ取,并生成返回值对象
return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
 } else if (!constructorMappings.isEmpty()) {
     
// 通过  节点配置的映射信息从 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) {
     
// 检测  是否配置了 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 中获取未配置在  中的列名
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);
// 为  中的列名拼接前缀
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 中。这样,列名的分拣工作就完成了。分拣过程示意图如下:
一万一千字!结合代码超详细讲解SQL执行流程(二)!干货到底!建议收藏!_第3张图片
如上图所示,实体类 Author 的 id 和 name 字段与列名 id 和 name 被配置在了
中,它们之间形成了映射关系。列名 age、sex 和 email 未配置在中,因此未与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
}

相关表记录如下
一万一千字!结合代码超详细讲解SQL执行流程(二)!干货到底!建议收藏!_第4张图片
接下来看一下 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">
 SELECT
 id, author_id, title, content, create_time
 FROM
 article
 WHERE
 id = #{
     id}
</select>
<select id="findAuthor" resultType="Author">
 SELECT
 id, name, age, sex, email
 FROM
 author
 WHERE
 id = #{
     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();
 }
 } }

测试结果如下:
一万一千字!结合代码超详细讲解SQL执行流程(二)!干货到底!建议收藏!_第5张图片

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

private Object getNestedQueryMappingValue(ResultSet rs, 
MetaObject metaResultObject, ResultMapping propertyMapping, 
ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
     
// 获取关联查询 id,id = 命名空间 +  的 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.  
* column 属性值仅包含列信息,参数类型为 author_id 列对应的类型,
* 这里为 Integer
* 2.  
* column 属性值包含了属性名与列名的复合信息,MyBatis 会根据列名从
* ResultSet 中获取列数据,并将列数据设置到实体类对象的指定属性中,比如:
* Author{id=1, name="MyBatis 源码分析系列文章导读", age=null, …}
* 或是以键值对 <属性, 列数据> 的形式,将两者存入 Map 中。比如:
* {"id": 1, "name": "MyBatis 源码分析系列文章导读"}
*
* 至于参数类型到底为实体类还是 Map,取决于关联查询语句的配置信息。比如:
*  
* -> 参数类型为实体类
*/
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 配置文件的
节点中加入或覆盖如下配置:

<!-- 开启延迟加载 -->
<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();
 }
 } }

测试结果如下:
一万一千字!结合代码超详细讲解SQL执行流程(二)!干货到底!建议收藏!_第6张图片

从上面结果中可以看出,我们在未调用 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 的源码大家自行分析吧啊,本节就不展开了。

你可能感兴趣的:(java,MySQL,Redis,程序员,编程)