懒加载定义

什么是Mybatis的懒加载机制?简单来说就是在使用时才会触发查询操作,而不是在一开始就加载所有数据。懒加载机制只在有子查询时生效,一定程度上可以提升程序的响应能力。

懒加载的整体结构

Mybatis懒加载结构 (1)

测试程序

还是基于Blog和Comment的测试程序,简单调整一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mybatis_reader.mybatis.blog.BlogMapper">

<resultMap id="BlogMap" type="org.example.mybatis_reader.mybatis.blog.BlogPO" autoMapping="true">
<id column="ID" property="id" jdbcType="INTEGER"/>
<result column="TITLE" property="title" jdbcType="VARCHAR"/>
<!-- 开启懒加载也很简单,只要fetchType="lazy"即可 />-->
<collection property="comments" column="id" select = "selectCommentByBlogId" fetchType="lazy"/>
</resultMap>

<resultMap id="commentMap" type="org.example.mybatis_reader.mybatis.blog.CommentPO" autoMapping="true" >
<id column="ID" property="id" jdbcType="INTEGER"/>
<result column="BLOG_ID" property="blogId" jdbcType="INTEGER"/>
<result column="CONTENT" property="content" jdbcType="VARCHAR"/>
</resultMap>

<select id="selectBlogById" resultMap="BlogMap">
SELECT * FROM T_BLOG WHERE ID = #{id}
</select>

<select id="selectCommentByBlogId" resultMap="commentMap">
SELECT * FROM T_COMMENT WHERE BLOG_ID = #{id}
</select>
</mapper>

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BlogPOTest {
private Configuration configuration;
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
sqlSessionFactory = sqlSessionFactoryBuilder.build(BlogMapper.class.getResourceAsStream("/mybatis-config.xml"));
configuration = sqlSessionFactory.getConfiguration();
}

@Test
public void test1(){
BlogMapper blogMapper = sqlSessionFactory.openSession().getMapper(BlogMapper.class);
BlogPO blogPO = blogMapper.selectBlogById(1L);
System.out.println("===================================");
System.out.println("执行其它大段逻辑");
System.out.println(blogPO.getComments());
}
}

看下测试结果,可以看到在执行访问操作时,才触发了查库操作,说明懒加载机制已经生效。

1
2
3
4
5
6
7
8
9
10
[main] DEBUG o.a.i.t.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5cc69cfe]
[main] DEBUG o.e.m.m.b.BlogMapper.selectBlogById - ==> Preparing: SELECT * FROM T_BLOG WHERE ID = ?
[main] DEBUG o.e.m.m.b.BlogMapper.selectBlogById - ==> Parameters: 1(Long)
[main] DEBUG o.e.m.m.b.BlogMapper.selectBlogById - <== Total: 1
===================================
执行其它大段逻辑
[main] DEBUG o.e.m.m.b.B.selectCommentByBlogId - ==> Preparing: SELECT * FROM T_COMMENT WHERE BLOG_ID = ?
[main] DEBUG o.e.m.m.b.B.selectCommentByBlogId - ==> Parameters: 1(Long)
[main] DEBUG o.e.m.m.b.B.selectCommentByBlogId - <== Total: 1
[CommentPO{id=1, blogId=1, userId=1, content='你说的对'}]

实现原理

属性赋值的流程中是如何兼容懒加载机制的?

在处理结果集的过程中,如果某个ResultMapping是嵌套查询并且声明了懒加载,那么会先为它创建一个代理;在后续流程中会构建一个LoadPari并放入到ResultLoaderMap类的一个HashMap中。

Mybatis的懒加载机制

简单看下这部分的源码:

DefaultResultSetHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader,
String columnPrefix) throws SQLException {
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())) {
// 获取所有的ResultMapping
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) { //遍历ResultMapping
// 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;
}

继续流程,依旧是getNestedQueryMappingValue方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
/** 省略代码*/
if (nestedQueryParameterObject != null) {
/** 省略代码*/
} else {
// 构建ResultLoader
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
nestedQueryParameterObject, targetType, key, nestedBoundSql);
if (propertyMapping.isLazy()) { // 判断是否支持懒加载
lazyLoader.addLoader(property, metaResultObject, resultLoader);
value = DEFERRED;
} else {
value = resultLoader.loadResult();
}
}
}
return value;
}

接着看ResultLoaderMap部分的逻辑,这里主要是构建LoadPair并将其维护在一个HashMap中。

1
2
3
4
5
6
7
8
9
10
11
12
private final Map<String, LoadPair> loaderMap = new HashMap<>();

public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
String upperFirst = getUppercaseFirstProperty(property);
if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
throw new ExecutorException("Nested lazy loaded result property '" + property + "' for query id '"
+ resultLoader.mappedStatement.getId()
+ " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
}
// 将结果加载器添加到懒加载器中
loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
}

可以看到整个流程还是很简单的。

访问时如何触发查询的呢?

现在我们看下在访问属性时,是如何通过代理完成查询的:

Mybatis的懒加载机制在访问时的流程

也简单看下源码。

JavassistProxyFactory

因为是代理类,直接看invoke方法总归是不会错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
final String methodName = method.getName();
// 加锁,放置并发时触发多次查询
lock.lock();
try {
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;
}
}
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
// 如果是激进模式,或者方法是触发懒加载的方法,则加载所有懒加载属性
lazyLoader.loadAll();
} else if (PropertyNamer.isSetter(methodName)) { // 是否是set()方法的访问
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property);
} else if (PropertyNamer.isGetter(methodName)) { // 是否是get()方法的访问
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) { // 判断懒加载器中是否有结果加载器
lazyLoader.load(property); // 调用结果加载器的load()方法完成数据加载。
}
}
}
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
} finally {
lock.unlock();
}
}

看下懒加载器中的load方法的逻辑,可以看到它会先将这个属性的结果处理器从懒加载器中删除,这意味着不论后续加载过程是否成功,我们再执行get()方法时也不会触发查库操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public boolean load(String property) throws SQLException {
// 从加载器中移除对应的结果处理器
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {
pair.load();
return true;
}
return false;
}

public void load(final Object userObject) throws SQLException {
if (this.metaResultObject == null || this.resultLoader == null) {
if (this.mappedParameter == null) {
throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
+ "required parameter of mapped statement [" + this.mappedStatement + "] is not serializable.");
}

final Configuration config = this.getConfiguration();
final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
if (ms == null) {
throw new ExecutorException(
"Cannot lazy load property [" + this.property + "] of deserialized object [" + userObject.getClass()
+ "] because configuration does not contain statement [" + this.mappedStatement + "]");
}

this.metaResultObject = config.newMetaObject(userObject);
this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
metaResultObject.getSetterType(this.property), null, null);
}

/*
* We are using a new executor because we may be (and likely are) on a new thread and executors aren't thread
* safe. (Is this sufficient?) A better approach would be making executors thread safe.
*/
if (this.serializationCheck == null) {
final ResultLoader old = this.resultLoader;
this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
}
// 重点是这里,通过结果处理器触发查询操作
this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}

ResultLoader

最后是调用Executor的query()方法执行具体的查询动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object loadResult() throws SQLException {
List<Object> list = selectList();
resultObject = resultExtractor.extractObjectFromList(list, targetType);
return resultObject;
}

private <E> List<E> selectList() throws SQLException {
Executor localExecutor = executor;
if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
localExecutor = newExecutor();
}
try {
// 开始查询数据
return localExecutor.query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER,
cacheKey, boundSql);
} finally {
if (localExecutor != executor) {
localExecutor.close(false);
}
}
}

总结

Mybatis的懒加载机制有助于我们优化程序性能,而且要使用它也很简单,只需要在对应的xml中添加fetchType=”lazy”就能开启懒加载机制。

它的实现原理也很清晰。简单来说就是在结果集处理结果时,如果我们开启了懒加载并且ResultMapping还是一个嵌套查询,那么就会为这个属性创建代理方法;随后为这个属性构造一个结果处理器并放入到懒加载中的集合属性中。在我们的程序访问这个属性时会,代理类会调用懒加载器的laod()方法,然后懒加载会从它的集合中移除这个属性对应的结果处理器,然后由结果处理器调用执行的queyr()方法完成查询操作。