文章目录
-
- 前言
- [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整合案例。