美文网首页
一万一千字!结合代码超详细讲解SQL执行流程(二)!干货到底!建

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

作者: 套马杆的程序员 | 来源:发表于2021-01-11 15:36 被阅读0次

    上文我们已经学习到查询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; }
    

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

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

    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. 按<resultMap>中配置的映射关系进行映射
      这三处代码的逻辑比较复杂,接下来按顺序进行分节说明。首先分析实体类的创建过程。

    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 的重载方法中,关于该方法,待
    会再分析。创建好实体类对后,还需要对<resultMap>中配置的映射信息进行检测。若发现
    有关联查询,且关联查询结果的加载方式为延迟加载,此时需为实体类生成代理类。举个
    例子说明一下,假设有如下两个实体类:

    /** 作者类 */
    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 - 自动映射所有

    除了以上三种等级,我们还可以显示配置<resultMap>节点的 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 方法用于检测是否应为当前结果集应用自动映射。检测
    结果取决于<resultMap>节点的 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用于记录未配置在<resultMap>节点中的映射关系。该类定义在 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 中获取未配置在<resultMap>中的列名
    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);
    }
    

    如上,已映射列名与未映射列名的分拣逻辑并不复杂,这里简述一下相关逻辑。首先是
    从当前数据集中获取列名集合,然后获取<resultMap>中配置的列名集合。之后遍历数据集中
    的列名集合,并判断列名是否被配置在了<resultMap>节点中。若配置了,则表明该列名已有
    映射关系,此时该列名存入 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 提供了两个标签用于支持一对一和一对多的使用场
    景,分别是<association>和<collection>。下面我来演示一下如何使用<association>完成一对一的关联查询。先来看看实体类的定义:

    /** 作者类 */
    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">
     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();
     }
     } }
    

    测试结果如下:


    在这里插入图片描述

    如上,从上面的输出结果中可以看出,我们在调用 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执行流程(二)!干货到底!建

          本文链接:https://www.haomeiwen.com/subject/bpedaktx.html