文章目录

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

10.4 小结

前言

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

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

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

10.3 MyBatis懒加载机制

10.3.1 懒加载机制的使用

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

这3个参数的作用如下:

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

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

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

select="com.star.mybatis.mapper.OrderMapper.listOrderByUserId"

ofType="Order"

javaType="List"

column="user_id">

单元测试:

@Test

public void testLazyQuery() {

User user = userMapper.getFullUserById(1);

System.out.println("完成User信息查询...");

List 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做了不同处理。

源码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()方法中可以发现有关懒加载的逻辑:

源码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;

}

// 处理等标签配置的映射

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实体对象。

源码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> constructorArgTypes = new ArrayList<>();

final List constructorArgs = new ArrayList<>();

// 创建Java实体对象

Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);

if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {

final List 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()方法处理等标签配置的映射。

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

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

源码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创建动态代理对象,其定义如下:

源码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> constructorArgTypes, List 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()方法加载该属性。

源码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整合案例。

文章链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: 

发表评论

返回顶部暗黑模式