目录
一、什么是延迟加载
在开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的订单信息。此时就是我们所说的延迟加载。
1、延迟加载的定义
延迟加载就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
2、优缺点
1)优点:
先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
2)缺点:
因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变⻓,造成用户体验下降。
3、什么时候用到
在多表中:
1)一对多,多对多:通常情况下采用延迟加载;
2)一对一(多对一):通常情况下采用立即加载。
4、注意:延迟加载是基于嵌套查询来实现的。
二、怎样实现
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>
注意:
局部的加载策略优先级高于全局的加载策略。
3、设置触发延迟加载的方法
大家在配置了延迟加载策略后,发现即使没有调用关联对象的任何方法,但是在你调用当前对象的 equals、clone、hashCode、toString 方法时也会触发关联对象的查询。
我们可以在配置文件中使用 lazyLoadTriggerMethods 配置项覆盖掉上面四个方法。
<settings>
<!-- 开启全局延迟加载功能 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 所有方法都会延迟加载 -->
<setting name="lazyLoadTriggerMethods" value=""/>
</settings>
三、延迟加载原理
1、理解描述
原理其实特别简单,就是在分析 User 的成员变量的时候,发现如果有懒加载的配置,如:fetchType=“lazy”,则把 User 转化成代理类返回。并把懒加载相关对象放到 ResultLoaderMap 中存起来。当调用相应 User 对象里面的 get 方法获取懒加载变量的时候则根据代理类去执行 sql 获取。
总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定方法,执行数据加载。
MyBatis 延迟加载主要使用:Javassist,Cglib实现,类图展示:
2、相关代码类
Configuration 类中相关属性:
public class Configuration {
/**
* aggressiveLazyLoading:
* 当开启时,任何方法的调用都会加载该对象的所有属性。
* 否则,每个属性会按需加载(参考 lazyLoadTriggerMethods)。
* 默认为 true
*/
protected boolean aggressiveLazyLoading;
// 延迟加载触发方法
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
// 是否开启延迟加载
protected boolean lazyLoadingEnabled = false;
/**
* 默认使用 Javassist 代理工厂
* @param proxyFactory
*/
public void setProxyFactory(ProxyFactory proxyFactory) {
if (proxyFactory == null) {
proxyFactory = new JavassistProxyFactory();
}
this.proxyFactory = proxyFactory;
}
// 省略。。
}
四、查看刨析源码
1、DefaultResultSetHandler
Mybatis 的查询结果是由 ResultSetHandler 接口的 handleResultSets() 方法处理的。ResultSetHandler 接口只有一个实现,DefaultResultSetHandler,接下来看下延迟加载相关的一个核心的方法。
/**
* 创建结果对象
* 标记调用路径:(都在DefaultResultSetHandler类中)
* handleResultSets -> handleResultSet -> handleRowValues ->
* handleRowValuesForNestedResultMap -> getRowValue -> createResultObject
*/
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader,
String columnPrefix) throws SQLException {
// reset previous mapping result
this.useConstructorMappings = false;
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;
}
}
}
// set current mapping result
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
return resultObject;
}
configuration 中默认的 proxyFactory 实现类是 JavassistProxyFactory,即默认采用 javassistProxy 进行代理对象的创建。
protected ProxyFactory proxyFactory = new JavassistProxyFactory();
2、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 {
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration,
ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
final Class<?> type = target.getClass();
EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration,
objectFactory, constructorArgTypes, constructorArgs);
Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
PropertyCopier.copyBeanProperties(type, target, enhanced);
return enhanced;
}
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 (LogHolder.log.isDebugEnabled()) {
LogHolder.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)) {
// 一次全部加载
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);
}
}
}
}
}
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
}
五、注意事项
1、IDEA 调试问题
当配置 aggressiveLazyLoading = true,在使用 IDEA 进行调试的时候,如果断点打到代理执行逻辑当中,你会发现延迟加载的代码永远都不能进入,总是会被提前执行。 主要产生的原因在 aggressiveLazyLoading,因为在调试的时候,IDEA 的 Debuger 窗体中已经触发了延迟加载对象的方法。
文章内容输出来源:拉勾教育Java高薪训练营;