我们在学习 MyBatis 框架时,会经常碰到一对一,一对多的使用场景。对于这样的场景,
通常我们可以用一条 SQL 进行多表查询完成任务。当然我们也可以使用关联查询,将一条
SQL 拆成两条去完成查询任务。MyBatis 提供了两个标签用于支持一对一和一对多的使用场
景,分别是和。

关联查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 作者类 */
public class Author {
private Integer id;
private String name;
private Integer age;
private Integer sex;
private String email;
// 省略 getter/setter
}
/** 文章类 */
public class Article {
private Integer id;
private String title;
// 一对一关系
private Author author;
private String content;
private Date createTime;
// 省略 getter/setter
}

看一下 Mapper 接口与映射文件的定义

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
public interface ArticleDao {
Article findOne(@Param("id") int id);
Author findAuthor(@Param("id") int authorId);
}

<mapper namespace="xyz.coolblog.chapter4.dao.ArticleDao">
<resultMap id="articleResult" type="Article">
<result property="createTime" column="create_time"/>
<association property="author" column="author_id"
javaType="Author" select="findAuthor"/>
</resultMap>

<select id="findOne" resultMap="articleResult">
SELECT
id, author_id, title, content, create_time
FROM
article
WHERE
id = #{id}
</select>

<select id="findAuthor" resultType="Author">
SELECT
id, name, age, sex, email
FROM
author
WHERE
id = #{id}
</select>
</mapper>

测试代码

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
public class OneToOneTest {
private SqlSessionFactory sqlSessionFactory;

@Before
public void prepare() throws IOException {
String resource = "chapter4/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}
@Test
public void testOne2One() {
SqlSession session = sqlSessionFactory.openSession();
try {
ArticleDao articleDao = session.getMapper(ArticleDao.class);
Article article = articleDao.findOne(1);
Author author = article.getAuthor();
article.setAuthor(null);
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\nauthor info:");
System.out.println(author);
} finally {
session.close();
}
}
}

image.png
从上面的输出结果中可以看出,我们在调用 ArticleDao 的 findOne 方法时,MyBatis
执行了两条 SQL,完成了一对一的查询需求。

MyBatis 是如何实现关联查询的。接下里从 getNestedQueryMappingValue 方法开始
分析,如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
private Object getNestedQueryMappingValue(ResultSet rs, 
MetaObject metaResultObject, ResultMapping propertyMapping,
ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
// 获取关联查询 id,id = 命名空间 + <association> 的 select 属性值
final String nestedQueryId = propertyMapping.getNestedQueryId();
final String property = propertyMapping.getProperty();
// 根据 nestedQueryId 获取 MappedStatement
final MappedStatement nestedQuery =
configuration.getMappedStatement(nestedQueryId);
// 获取nestedQuery的参数类型
final Class<?> nestedQueryParameterType =
nestedQuery.getParameterMap().getType();
/*
* 生成关联查询语句参数对象,参数类型可能是一些包装类,Map 或是自定义的实体类,
* 具体类型取决于配置信息。以上面的例子为基础,下面分析不同配置对
* 参数类型的影响:
* 1. <association column="author_id">
* column 属性值仅包含列信息,参数类型为 author_id 列对应的类型,
* 这里为 Integer
* 2. <association column="{id=author_id, name=title}">
* column 属性值包含了属性名与列名的复合信息,MyBatis 会根据列名从
* ResultSet 中获取列数据,并将列数据设置到实体类对象的指定属性中,比如:
* Author{id=1, name="MyBatis 源码分析系列文章导读", age=null, …}
* 或是以键值对 <属性, 列数据> 的形式,将两者存入 Map 中。比如:
* {"id": 1, "name": "MyBatis 源码分析系列文章导读"}
*
* 至于参数类型到底为实体类还是 Map,取决于关联查询语句的配置信息。比如:
* <select id="findAuthor"> -> 参数类型为 Map
* <select id="findAuthor" parameterType="Author">
* -> 参数类型为实体类
*/
final Object nestedQueryParameterObject=prepareParameterForNestedQuery(
rs, propertyMapping, nestedQueryParameterType, columnPrefix);
Object value = null;
if (nestedQueryParameterObject != null) {
// 获取 BoundSql
final BoundSql nestedBoundSql =
nestedQuery.getBoundSql(nestedQueryParameterObject);
final CacheKey key = executor.createCacheKey(nestedQuery,
nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
final Class<?> targetType = propertyMapping.getJavaType();
// 检查一级缓存是否保存了关联查询结果
if (executor.isCached(nestedQuery, key)) {
// 从一级缓存中获取关联查询的结果,并通过 metaResultObject
// 将结果设置到相应的实体类对象中
executor.deferLoad(nestedQuery,
metaResultObject, property, key, targetType);
value = DEFERED;
} else {
// 创建结果加载器
final ResultLoader resultLoader = new ResultLoader(
configuration, executor, nestedQuery,
nestedQueryParameterObject, targetType, key, nestedBoundSql);
// 检测当前属性是否需要延迟加载
if (propertyMapping.isLazy()) {
// 添加延迟加载相关的对象到 loaderMap 集合中
lazyLoader.addLoader(
property, metaResultObject, resultLoader);
value = DEFERED;
} else {
// 直接执行关联查询
value = resultLoader.loadResult();
}
}
}
return value;
}
  1. 根据 nestedQueryId 获取 MappedStatement
  2. 生成参数对象
  3. 获取 BoundSql
  4. 检测一级缓存中是否有关联查询的结果,若有,则将结果设置到实体类对象中
  5. 若一级缓存未命中,则创建结果加载器 ResultLoader
  6. 检测当前属性是否需要进行延迟加载,若需要,则添加延迟加载相关的对象到loaderMap 集合中
  7. 如不需要延迟加载,则直接通过结果加载器加载结果

getNestedQueryMappingValue方法中逻辑多是都是和延迟加载有关。除了延迟加载,以上流程中针对一级缓存的检查是十分有必要的

  • 若缓存命中,可直接取用结果,无需再在执行关联查询SQL。
  • 若缓存未命中,接下来就要按部就班执行延迟加载相关逻辑

延迟加载

添加延迟加载相关对象到loaderMap 集合中的逻辑,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// -☆- ResultLoaderMap
public void addLoader(String property, MetaObject metaResultObject,
ResultLoader resultLoader) {
// 将属性名转为大写
String upperFirst = getUppercaseFirstProperty(property);
// 属性名不相等或者loaderMap包含该属性
if (!upperFirst.equalsIgnoreCase(property) &&
loaderMap.containsKey(upperFirst)) {
throw new ExecutorException("……");
}
// 创建 LoadPair,并将 <大写属性名,LoadPair 对象> 键值对添加到 loaderMap 中
loaderMap.put(upperFirst, new LoadPair(property, metaResultObject,
resultLoader));
}

addLoader 方法的参数最终都传给了LoadPair,该类的load方法会在内部调用ResultLoader 的loadResult方法进行关联查询,并通过metaResultObject将查询结果设置到实体类对象中。

LoadPair 的 load 方法由谁调用呢?答案是实体类的代理对象。

实例

首先,我们需要在 MyBatis 配置文件的节点中加入或覆盖如下配置:

1
2
3
4
5
6
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 关闭积极的加载策略 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 延迟加载的触发方法 -->
<setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>

测试类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class OneToOneTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void prepare() throws IOException {...}
@Test
public void testOne2One2() {
SqlSession session = sqlSessionFactory.openSession();
try {
ArticleDao articleDao = session.getMapper(ArticleDao.class);
Article article = articleDao.findOne(1);
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\n 延迟加载 author 字段:");
// 通过 getter 方法触发延迟加载
Author author = article.getAuthor();
System.out.println("\narticles info:");
System.out.println(article);
System.out.println("\nauthor info:");
System.out.println(author);
} finally {
session.close();
}
}
}

image.png
从上面结果中可以看出,我们在未调用 getAuthor 方法时,Article 对象中的 author 字段为 null。调用该方法后,再次输出 Article 对象,发现其 author 字段有值了,表明 author 字段的延迟加载逻辑被触发了。

既然调用 getAuthor 可以触发延迟加载,那么该方法一定被做过手脚了,不然该方法应该返回 null 才是。实际情况确实如此,MyBatis会为需要延迟加载的类生成代理类,代理逻辑会拦截实体类的方法调用。默认情况下,MyBatis会使用Javassist为实体类生成代理,代理逻辑封装在 JavassistProxyFactory 类中

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
// -☆- JavassistProxyFactory
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)) {
// 针对 writeReplace 方法的处理逻辑,与延迟加载无关,不分析了
} else {
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
// 如果 aggressive 为 true,或触发方法(比如 equals,
// hashCode 等)被调用,则加载所有的所有延迟加载的数据
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
} else if (PropertyNamer.isSetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
// 如果使用者显示调用了 setter 方法,则将相应的
// 延迟加载类从 loaderMap 中移除
lazyLoader.remove(property);
// 检测使用者是否调用 getter 方法
} else if (PropertyNamer.isGetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
// 检测该属性是否有相应的 LoadPair 对象
if (lazyLoader.hasLoader(property)) {
// 执行延迟加载逻辑
lazyLoader.load(property);
}
}
}
}
}
// 调用被代理类的方法
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}

代理方法首先会检查 aggressive是否为true,如果不满足,再去检查lazyLoadTriggerMethods 是否包含当前方法名。这里两个条件只要一个为true,当���实体类中所有需要延迟加载

aggressive 和 lazyLoadTriggerMethods 两个变量的值取决于下面的配置。

1
2
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>

如果执行线程未进入第一个条件分支,那么紧接着,代理逻辑会检查使用者是不是调用了实体类的 setter 方法。如果调用了,就将该属性对应的 LoadPair 从loaderMap 中移除。

使用者既然手动调用setter方法,说明使用者想自定义某个属性的值。此时,延迟加载逻辑不应该再修改该属性的值,所以这里从loaderMap 中移除属性对于的LoadPair。

最后如果使用者调用的是某个属性的getter方法,且该属性配置了延迟加载,此时延迟加载逻辑就会被触发。

延迟加载逻辑是怎样实现的

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
// -☆- ResultLoaderMap
public boolean load(String property) throws SQLException {
// 从 loaderMap 中移除 property 所对应的 LoadPair
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {
// 加载结果
pair.load();
return true;
}
return false;
}

// -☆- LoadPair
public void load() throws SQLException {
if (this.metaResultObject == null) {
throw new IllegalArgumentException("metaResultObject is null");
}
if (this.resultLoader == null) {
throw new IllegalArgumentException("resultLoader is null");
}
// 调用重载方法
this.load(null);
}

public void load(final Object userObject) throws SQLException {
// 若 metaResultObject 和 resultLoader 为 null,则创建相关对象。
// 在当前调用情况下,两者均不为 null,条件不成立。篇幅原因,下面代码不分析了
if (this.metaResultObject == null || this.resultLoader == null) {...}
// 线程安全检测
if (this.serializationCheck == null) {
final ResultLoader old = this.resultLoader;
// 重新创建新的 ResultLoader 和 ClosedExecutor,
// ClosedExecutor 是非线程安全的
this.resultLoader = new ResultLoader(old.configuration,
new ClosedExecutor(), old.mappedStatement, old.parameterObject,
old.targetType, old.cacheKey, old.boundSql);
}
// 调用 ResultLoader 的 loadResult 方法加载结果,
// 并通过 metaResultObject 设置结果到实体类对象中
this.metaResultObject.setValue(property,this.resultLoader.loadResult());
}

重点关注最后一行有效代码,ResultLoader 的 loadResult 方法逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 {
// 通过 Executor 就行查询,这个之前已经分析过了
return localExecutor.<E>query(mappedStatement, parameterObject,
RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey,boundSql);
} finally {
if (localExecutor != executor) {
localExecutor.close(false);
}
}
}

我们在 ResultLoader 中终于看到了执行关联查询的代码,即 selectList 方法中的
逻辑。该方法在内部通过 Executor 进行查询。