MyBatis3源码深度解析(二十六)级联映射与关联查询(三)懒加载的使用与实现原理

文章目录

    • 前言
    • [10.3 MyBatis懒加载机制](#10.3 MyBatis懒加载机制)
      • [10.3.1 懒加载机制的使用](#10.3.1 懒加载机制的使用)
      • [10.3.2 懒加载的实现原理](#10.3.2 懒加载的实现原理)
        • [10.3.2.1 创建Java实体代理对象](#10.3.2.1 创建Java实体代理对象)
        • [10.3.2.2 保存外部Mapper配置信息](#10.3.2.2 保存外部Mapper配置信息)
        • [10.3.2.3 执行拦截逻辑](#10.3.2.3 执行拦截逻辑)
        • [10.3.2.4 总结](#10.3.2.4 总结)
    • [10.4 小结](#10.4 小结)

前言

上一节【MyBatis3源码深度解析(二十五)级联映射与关联查询(二)级联映射的实现原理】详细解读了MyBatis级联映射的实现原理,在使用外部Mapper的方式实现级联映射时,会为关联的Java实体对象执行一次额外的查询。

但在一些场景下,可能会需要按需加载。例如查询用户信息时,不需要立刻查询用户关联的订单信息,而是在调用订单信息的Getter方法时,才执行订单信息的查询操作。

MyBatis提供了懒加载机制来实现这种需求,这种方式能够在一定程度上减少数据库的IO次数,提升系统性能。

10.3 MyBatis懒加载机制

10.3.1 懒加载机制的使用

在MyBatis的主配置文件中,提供了lazyLoadingEnabled、aggressiveLazyLoading和lazyLoadTriggerMethods参数来控制懒加载机制。

xml 复制代码
<!--mybatis-config.xml-->
<!--打开懒加载的开关-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--配置ResultMap的加载行为是懒加载-->
<setting name="aggressiveLazyLoading" value="false"/>
<!--配置不触发懒加载的方法-->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>

这3个参数的作用如下:

  • lazyLoadingEnabled:这个参数可以理解为懒加载的开关,当lazyLoadingEnabled=true时,表示开启懒加载。默认值为false。
  • aggressiveLazyLoading:这个参数用于控制ResultMap默认的加载行为,为false时表示ResultMap默认的加载行为是懒加载,否则为积极加载。默认值为false。
  • lazyLoadTriggerMethods :这个参数用于配置不处罚懒加载的方法,默认值是equals,clone,hashCode,toString

另外,<collection>和<association>标签还提供了一个fetchType属性 ,用于控制级联查询的加载行为。当fetchType=lazy时,表示该级联查询采用懒加载方式;当fetchType=eager时,表示该级联查询采用积极加载方式。如果没有配置fetchType属性,则该属性值与主配置文件的lazyLoadingEnabled参数相同。

在MyBatis主配置文件开启懒加载后,下面编写一个测试案例,:

xml 复制代码
<!--UserMapper.xml-->
<resultMap id="fullUser" type="User" autoMapping="true">
    <id column="user_id" property="userId"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <result column="phone" property="phone"/>
    <result column="birthday" property="birthday"/>
    <collection property="orderList"
                select="com.star.mybatis.mapper.OrderMapper.listOrderByUserId"
                ofType="Order"
                javaType="List"
                column="user_id">
    </collection>
</resultMap>

单元测试:

java 复制代码
@Test
public void testLazyQuery() {
    User user = userMapper.getFullUserById(1);
    System.out.println("完成User信息查询...");
    List<Order> orderList = user.getOrderList();
    System.out.println("orderList.size = " + orderList.size());
}

控制台打印执行结果:

Opening JDBC Connection
Created connection 271800170.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1033576a]
==>  Preparing: select * from user where user_id = ?
==> Parameters: 1(Integer)
<==    Columns: user_id, name, age, phone, birthday
<==        Row: 1, 孙悟空, 1500, 18705464523, 0001-01-01 00:00:00.0
<==      Total: 1
完成User信息查询...
==>  Preparing: select * from `order` where user_id = ?
==> Parameters: 1(Integer)
<==    Columns: order_id, user_id, order_no, address, amount
<==        Row: 1, 1, order_01, 广东广州, 100
<==        Row: 2, 1, order_02, 广东河源, 200
<==      Total: 2
orderList.size = 2

由执行结果可知,在调用UserMapper的getFullUserById()方法后,只查询了用户的信息,而没有查询用户关联订单的信息;当调用User对象的getOrderList()方法时,才查询订单信息。这就实现了懒加载。

10.3.2 懒加载的实现原理

强烈建议在学习懒加载的实现原理之前,学习一下上一节【MyBatis3源码深度解析(二十五)级联映射与关联查询(二)级联映射的实现原理】的内容,因为分析的源码都是一样的,MyBatis只是在级联映射的实现过程中穿插了一些懒加载的逻辑。

因此下文的源码分析会比较跳跃,只选择有关懒加载的源码进行解读。

10.3.2.1 创建Java实体代理对象

首先,在DefaultResultSetHandler类的handleRowValues()方法中,对嵌套ResultMap和非嵌套ResultMap做了不同处理。

java 复制代码
源码1:org.apache.ibatis.executor.resultset.DefaultResultSetHandler

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
        RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
        // 嵌套ResultMap的处理
        ensureNoRowBounds();
        checkResultHandler();
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
        // 非嵌套ResultMap的处理
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}

而要实现懒加载,必然是使用外部Mapper的方式来实现级联映射(非嵌套ResultMap),因为使用JOIN子句的方式会立即查询关联表。因此,开启懒加载之后,handleRowValues()方法会调用handleRowValuesForSimpleResultMap()方法。

根据上一节的分析思路进行溯源,在getRowValue()方法中可以发现有关懒加载的逻辑:

java 复制代码
源码2:org.apache.ibatis.executor.resultset.DefaultResultSetHandler

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    // 创建ResultLoaderMap对象,用于存放懒加载属性信息
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // 创建ResultMap对应的Java实体对象
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    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, columnPrefix) || foundValues;
        }
        // 处理<result>等标签配置的映射
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}

由 源码2 可知,getRowValue()方法首先创建了一个ResultLoaderMap对象,用于存放懒加载属性信息,接着调用createResultObject()方法创建ResultMap对应的Java实体对象。

java 复制代码
源码3:org.apache.ibatis.executor.resultset.DefaultResultSetHandler

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader,
                                  String columnPrefix) throws SQLException {
    this.useConstructorMappings = false;
    final List<Class<?>> constructorArgTypes = new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    // 创建Java实体对象
    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) {
            if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                // 如果有嵌套查询且开启了懒加载,则创建代理对象
                resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration,
                        objectFactory, constructorArgTypes, constructorArgs);
                break;
            }
        }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
    return resultObject;
}

由 源码3 可知,调用createResultObject()方法创建ResultMap对应的Java实体对象时,如果有嵌套查询且开启了懒加载,则会调用ProxyFactory实例的createProxy()覆盖创建一个代理对象,覆盖原来已经创建好的Java实体对象。

也就是说,开启懒加载后,创建的Java实体对象是一个代理对象。

10.3.2.2 保存外部Mapper配置信息

回到 源码2,继续执行applyAutomaticMappings()方法处理自动映射,执行applyPropertyMappings()方法处理<result>等标签配置的映射。

applyPropertyMappings()方法中,又会调用getPropertyMappingValue()方法获取数据库字段的值。

getPropertyMappingValue()方法中,如果ResultMapping对象的nestedQueryId属性值不为空,说明配置了外部Mapper,则会调用getNestedQueryMappingValue()方法执行这个外部Mapper。

java 复制代码
源码4:org.apache.ibatis.executor.resultset.DefaultResultSetHandler

private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
                                          ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    
    // ......
    
    if (propertyMapping.isLazy()) {
        // 如果开启了懒加载,则将外部Mapper的执行操作记录在ResultLoaderMap对象中
        lazyLoader.addLoader(property, metaResultObject, resultLoader);
        value = DEFERRED;
    } else {
        // 没有开启懒加载,直接执行Mapper
        value = resultLoader.loadResult();
    }
    return value;
}

由 源码4 可知,在getNestedQueryMappingValue()方法中,关于懒加载的关键逻辑是:如果开启了懒加载,则将外部Mapper的执行操作记录在ResultLoaderMap对象中;如果没有开启懒加载,直接执行Mapper。

10.3.2.3 执行拦截逻辑

开启懒加载后,由于执行查询操作得到的Java实体是一个代理对象,因此调用代理对象的Getter方法时,会调用相应的拦截逻辑。

MyBatis同时支持使用Cglib和Javassist创建代理对象,具体使用哪种策略,可以在MyBatis主配置文件中通过proxyFactory属性指定。

假设使用了Cglib创建动态代理对象,其定义如下:

java 复制代码
源码5:org.apache.ibatis.executor.loader.cglib.CglibProxyFactory

public class CglibProxyFactory implements ProxyFactory {
    
    @Override
    public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration,
            ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory,
            constructorArgTypes, constructorArgs);
    }
    
    private static class EnhancedResultObjectProxyImpl implements MethodInterceptor {
    
        @Override
        public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            final String methodName = method.getName();
            try {
                synchronized (lazyLoader) {
                    // ......
                    if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
                        if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                            // ......
                        } else if (PropertyNamer.isGetter(methodName)) {
                            final String property = PropertyNamer.methodToProperty(methodName);
                            // 判断该属性是否是懒加载属性,如果是则加载该属性
                            if (lazyLoader.hasLoader(property)) {
                                lazyLoader.load(property);
                            }
                        }
                    }
                }
                return methodProxy.invokeSuper(enhanced, args);
            } // catch ......
        }
    }
}

由 源码5 可知,使用Cglib创建动态代理对象时,在调用动态代理对象的Getter方法时,会执行EnhancedResultObjectProxyImpl类的intercept()方法中定义的拦截逻辑。

intercept()方法中,会调用ResultLoaderMap对象的hasLoader()方法判断该属性是否是懒加载属性,如果是,则调用ResultLoaderMap对象的load()方法加载该属性。

java 复制代码
源码6:org.apache.ibatis.executor.loader.ResultLoaderMap

public class ResultLoaderMap {

    public boolean load(String property) throws SQLException {
        LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
        if (pair != null) {
            pair.load();
            return true;
        }
        return false;
    }

    private LoadPair(final String property, MetaObject metaResultObject, ResultLoader resultLoader) {
    
        public void load(final Object userObject) throws SQLException {
            // ......
            this.metaResultObject.setValue(property, this.resultLoader.loadResult());
        }
    }
}

由 源码6 可知,ResultLoaderMap对象的load()方法会转调其内部类LoadPair的load()方法。

LoadPair的load()方法又会创建一个ResultLoader对象,并调用其loadResult()方法,该方法最终会调用Executor的query()方法执行外部Mapper定义的查询操作,为属性赋值。

10.3.2.4 总结

MyBatis的懒加载实际上是通过动态代理来实现的。当通过MyBatis的配置开启懒加载后,执行第一次查询操作实际上返回的是通过Cglib或者Javassist创建的代理对象。

因此,调用代理对象的Getter方法获取懒加载属性时,会执行动态代理的拦截方法。

在拦截方法中,通过Getter方法名获取Java实体属性名称,然后根据属性名称获取对应的LoadPair对象,LoadPair对象中维护了Mapper的ID,根据Mapper的ID获取对应的MappedStatement对象,接着执行一次额外的查询操作,使用查询结果为懒加载属性赋值。

10.4 小结

第十章到此就梳理完毕了,本章的主题是:MyBatis级联映射与懒加载。回顾一下本章的梳理的内容:

(二十四)级联映射的使用

(二十五)级联映射的实现原理

(二十六)懒加载的使用与实现原理

更多内容请查阅分类专栏:MyBatis3源码深度解析

第十一章主要学习:MyBatis与Spring整合案例

相关推荐
leegong2311139 分钟前
PostgreSQL 初中级认证可以一起学吗?
数据库
漫漫进阶路2 小时前
VS C++ 配置OPENCV环境
开发语言·c++·opencv
陈平安Java and C2 小时前
MyBatisPlus
java
秋野酱2 小时前
如何在 Spring Boot 中实现自定义属性
java·数据库·spring boot
weisian1513 小时前
Mysql--实战篇--@Transactional失效场景及避免策略(@Transactional实现原理,失效场景,内部调用问题等)
数据库·mysql
安的列斯凯奇3 小时前
SpringBoot篇 单元测试 理论篇
spring boot·后端·单元测试
AI航海家(Ethan)3 小时前
PostgreSQL数据库的运行机制和架构体系
数据库·postgresql·架构
Bunny02123 小时前
SpringMVC笔记
java·redis·笔记
架构文摘JGWZ3 小时前
FastJson很快,有什么用?
后端·学习
BinaryBardC3 小时前
Swift语言的网络编程
开发语言·后端·golang