一. 延迟加载的介绍及应用
(二)什么是延迟加载?
问题
在开发过程中很多时候我们并不需要总是在加载⽤户信息时就⼀定要加载他的订单信息。此时就是我们所说的延迟加载。
举个例子:
* 在⼀对多中,当我们有⼀个⽤户,它有个 100 个订单在查询⽤户的时候,要不要把关联的订单查出来?在查询订单的时候,要不要把关联的⽤户查出来?* 回答在查询⽤户时,⽤户下的订单应该是,什么时候⽤,什么时候查询。在查询订单时,订单所属的⽤户信息应该是随着订单⼀起查询出来。
延迟加载
就是在需要⽤到数据时才进⾏加载,不需要⽤到数据时就不加载数据。延迟加载也称懒加载。
* 优点:先从单表查询,需要时再从关联表去关联查询,⼤⼤提⾼数据库性能,因为查询单表要⽐关联查询多张表速度要快。* 缺点:因为只有当需要⽤到数据时,才会进⾏数据库查询,这样在⼤批量数据查询时,因为查询⼯作也要消耗时间,所以可能造成⽤户等待时间变⻓,造成⽤户体验下降。* 在多表中:⼀对多,多对多:通常情况下采⽤延迟加载⼀对⼀(多对⼀):通常情况下采⽤⽴即加载* 注意:延迟加载是基于嵌套查询来实现的
(二)延迟加载的使用
1. 局部延迟加载
在
association
和
collection
标签中都有⼀个
fetchType
属性,通过修改它的值,可以修改局部的加载策略。
<!-- 开启⼀对多 延迟加载 -->
<resultMap id="userMap" type="user">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<!--
fetchType="lazy" 懒加载策略
fetchType="eager" ⽴即加载策略
-->
<collection property="orderList" ofType="order" column="id"
select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy">
</collection>
</resultMap> <select id="findAll" resultMap="userMap">
SELECT * FROM `user`
</select>
2. 全局延迟加载
在
Mybatis
的核⼼配置⽂件中可以使⽤
setting
标签修改全局的加载策略。
<settings>
<!--开启全局延迟加载功能-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
注意:
<!-- 关闭⼀对⼀ 延迟加载 -->
<resultMap id="orderMap" type="order">
<id column="id" property="id"></id>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
<!--
fetchType="lazy" 懒加载策略
fetchType="eager" ⽴即加载策略
-->
<association property="user" column="uid" javaType="user"
select="com.lagou.dao.UserMapper.findById" fetchType="eager">
</association>
</resultMap> <select id="findAll" resultMap="orderMap">
SELECT * from orders
</select>
二. 延迟加载原理
它的原理是,使⽤
CGLIB
或
Javassist(
默认
)
创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属性的getting
⽅法时,
进⼊拦截器⽅法
。⽐如调⽤
a.getB().getName()
⽅法,进⼊拦截器的
invoke(...)
⽅法,
发现
a.getB()
需要延迟加载时,那么就会单独发送事先保存好的查询关联
B
对象的
SQL
,把
B
查询上来,然后
调⽤
a.setB(b)
⽅法,于是
a
对象
b
属性就有值了,接着完成
a.getB().getName()
⽅法的调⽤。这就是延迟
加载的基本原理 。
总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。
三. 延迟加载源码分析
MyBatis
延迟加载主要使⽤:
Javassist
,
Cglib
实现,类图展示:
(一)Setting 配置加载:
/**
* MyBatis 配置
*
* @author Clinton Begin
*/
public class Configuration {
/**
* DB Environment 对象
*/
protected Environment environment;
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
/**
* 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载(参考lazyLoadTriggerMethods)
*/
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
/**
* 是否返回空行对应的对象
*/
protected boolean returnInstanceForEmptyRow;
protected String logPrefix;
protected Class<? extends Log> logImpl;
/**
* VFS 实现类
*/
protected Class<? extends VFS> vfsImpl;
/**
* {@link BaseExecutor} 本地缓存范围
*/
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
/**
* 指定哪个对象的方法触发一次延迟加载。
*/
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
/**
* 自动映射行为
*/
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
/**
* 自动映射失败的行为处理
*/
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
/**
* 变量 Properties 对象。
*
* 参见 {@link org.apache.ibatis.builder.xml.XMLConfigBuilder#propertiesElement(XNode context)} 方法
*/
protected Properties variables = new Properties();
/**
* ReflectorFactory 对象
*/
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
/**
* ObjectFactory 对象
*/
protected ObjectFactory objectFactory = new DefaultObjectFactory();
/**
* ObjectWrapperFactory 对象
*/
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
/**
* 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。
*/
protected boolean lazyLoadingEnabled = false;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
...
}
(二)延迟加载代理对象创建
Mybatis
的查询结果是由
ResultSetHandler
接⼝的
handleResultSets()
⽅法处理的。
ResultSetHandler
接⼝只有⼀个实现,
DefaultResultSetHandler
,接下来看下延迟加载相关的⼀个核⼼的⽅法。
// 创建映射后的结果对象
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
// useConstructorMappings ,表示是否使用构造方法创建该结果对象。此处将其重置
this.useConstructorMappings = false; // reset previous mapping result
final List<Class<?>> constructorArgTypes = new ArrayList<>(); // 记录使用的构造方法的参数类型的数组
final List<Object> constructorArgs = new ArrayList<>(); // 记录使用的构造方法的参数值的数组
// 创建映射后的结果对象
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) {
// issue gcode #109 && issue #149
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
// 判断是否使用构造方法创建该结果对象
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
默认采⽤
javassistProxy
进⾏代理对象的创建
JavasisstProxyFactory实现
public class JavassistProxyFactory implements
org.apache.ibatis.executor.loader.ProxyFactory {
/**
* 接⼝实现
* @param target ⽬标结果对象
* @param lazyLoader 延迟加载对象
* @param configuration 配置
* @param objectFactory 对象⼯⼚
* @param constructorArgTypes 构造参数类型
* @param constructorArgs 构造参数值
* @return
*/
@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 MethodHandler {
/**
* 创建代理对象
* @param type
* @param callback
* @param constructorArgTypes
* @param constructorArgs
* @return
*/
static Object crateProxy(Class<?> type, MethodHandler callback,
List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
ProxyFactory enhancer = new ProxyFactory();
enhancer.setSuperclass(type);
try {
//通过获取对象⽅法,判断是否存在该⽅法
type.getDeclaredMethod(WRITE_REPLACE_METHOD);
// ObjectOutputStream will call writeReplace of objects returned by writeReplace
if (log.isDebugEnabled()) {
log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type
+ ", make sure it returns this");
}
} catch (NoSuchMethodException e) {
//没找到该⽅法,实现接⼝
enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
} catch (SecurityException e) {
// nothing to do here
}
Object enhanced;
Class<?>[] typesArray = constructorArgTypes.toArray(new
Class[constructorArgTypes.size()]);
Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
try {
//创建新的代理对象
enhanced = enhancer.create(typesArray, valuesArray);
} catch (Exception e) {
throw new ExecutorException("Error creating lazy proxy. Cause: " + e, e);
}
//设置代理执⾏器
((Proxy) enhanced).setHandler(callback);
return enhanced;
}
/**
* 代理对象执⾏
* @param enhanced 原对象
* @param method 原对象⽅法
* @param methodProxy 代理⽅法
* @param args ⽅法参数
* @return
* @throws Throwable
*/
@Override
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)) {
//忽略暂未找到具体作⽤
Object original;
if (constructorArgTypes.isEmpty()) {
original = objectFactory.create(type);
} else {
original = objectFactory.create(type, constructorArgTypes,
constructorArgs);
}
PropertyCopier.copyBeanProperties(type, enhanced, original);
if (lazyLoader.size() > 0) {
return new JavassistSerialStateHolder(original,
lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
} else {
return original;
}
} else {
//延迟加载数量⼤于0
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
//aggressive ⼀次加载性所有需要要延迟加载属性或者包含触发延迟加载⽅法
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
log.debug("==> laze lod trigger method:" + methodName + ",proxy method:" + methodProxy.getName() + " class:" +
enhanced.getClass());
//⼀次全部加载
lazyLoader.loadAll();
} else if (PropertyNamer.isSetter(methodName)) {
//判断是否为set⽅法,set⽅法不需要延迟加载
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property);
} else if (PropertyNamer.isGetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
//延迟加载单个属性
lazyLoader.load(property);
log.debug("load one :" + methodName);
}
}
}
}
}
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
注意事项
IDEA
调试问题 当配置
aggressiveLazyLoading=true
,在使⽤
IDEA
进⾏调试的时候,如果断点打到代理执⾏逻
辑当中,你会发现延迟加载的代码永远都不能进⼊,总是会被提前执⾏。 主要产⽣的原因在
aggressiveLazyLoading
,因为在调试的时候,
IDEA
的
Debuger
窗体中已经触发了延迟加载对象的⽅法。
节选自拉钩教育JAVA系列教程