在之前的文章中,已经大概总结了简单映射的实现原理,在实际使用Mybatis的过程中,往往对象格式是更加复杂的。
使用示例

以上的表结构是一个简单的博客文章模型,文章关联作者,评论关联文章和作者,为此,我们先完成对应的JavaBean
对象封装。
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
|
@Data
public class Author {
private Integer id;
private String name;
}
@Data
public class Comment {
private Integer id;
private String body;
private Integer postId;
private Author author;
}
@Data
public class Post {
private Integer id;
private String title;
private Author author;
private List<Comment> comments;
}
|
如果要查询一篇文章的所有信息,ResultMapping
该如何配置呢?
编写SQL语句(使用JOIN的方式)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
SELECT p.id AS id,
p.title AS title,
a.id AS author_id,
a.name AS author_name,
c.id AS comment_id,
c.body AS comment_body,
ca.id AS comment_author_id,
ca.name AS comment_author_name
FROM post p
LEFT JOIN author a ON a.id = p.author_id
LEFT JOIN comment c ON c.post_id = p.id
LEFT JOIN author ca ON ca.id = c.author_id
WHERE p.id = 1;
|
查询结果如下:

配置ResultMap
以及编写PostMapper
:
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
|
<?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="com.wuwenze.mybatis.example2.mapper.PostMapper">
<resultMap id="joinResultMap" type="com.wuwenze.mybatis.example2.entity.Post">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="author"
javaType="com.wuwenze.mybatis.example2.entity.Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
</association>
<collection property="comments"
ofType="com.wuwenze.mybatis.example2.entity.Comment">
<id property="id" column="comment_id"/>
<result property="body" column="comment_body"/>
<association property="author"
javaType="com.wuwenze.mybatis.example2.entity.Author">
<id property="id" column="comment_author_id"/>
<result property="name" column="comment_author_name"/>
</association>
</collection>
</resultMap>
<select id="findById" resultMap="joinResultMap">
SELECT p.id AS id,
p.title AS title,
a.id AS author_id,
a.name AS author_name,
c.id AS comment_id,
c.body AS comment_body,
ca.id AS comment_author_id,
ca.name AS comment_author_name
FROM post p
LEFT JOIN author a ON a.id = p.author_id
LEFT JOIN comment c ON c.post_id = p.id
LEFT JOIN author ca ON ca.id = c.author_id
WHERE p.id = #{id};
</select>
</mapper>
|
编写单元测试,查看运行结果:
1
2
3
4
5
6
7
8
9
10
11
|
public interface PostMapper {
Post findById(@Param("id") Integer id);
}
@Test
public void test1() {
final PostMapper postMapper = sqlSession.getMapper(PostMapper.class);
final Post post = postMapper.findById(1);
System.out.println(JSONUtil.toJsonPrettyStr(post));
}
|
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
|
==> Preparing: SELECT p.id AS id, p.title AS title, a.id AS author_id, a.name AS author_name, c.id AS comment_id, c.body AS comment_body, ca.id AS comment_author_id, ca.name AS comment_author_name FROM post p LEFT JOIN author a ON a.id = p.author_id LEFT JOIN comment c ON c.post_id = p.id LEFT JOIN author ca ON ca.id = c.author_id WHERE p.id = ?;
==> Parameters: 1(Integer)
<== Columns: id, title, author_id, author_name, comment_id, comment_body, comment_author_id, comment_author_name
<== Row: 1, How to use mybatis?, 1, Zhang, 1, haha~, 2, Wang
<== Row: 1, How to use mybatis?, 1, Zhang, 2, I don't know., 3, Tom
<== Total: 2
{
"comments": [
{
"author": {
"name": "Wang",
"id": 2
},
"body": "haha~",
"id": 1
},
{
"author": {
"name": "Tom",
"id": 3
},
"body": "I don't know.",
"id": 2
}
],
"author": {
"name": "Zhang",
"id": 1
},
"title": "How to use mybatis?",
"id": 1
}
|
实现原理
Mybatis是如何将查询结果的两条复合结果数据,填充为一个JavaBean对象的呢?从上面的测试来看,在嵌套映射时,对象呈树形结构,对之对应的ResultMap
嵌套结构亦是如此:

除了以上演示的子映射配置方式外,还可以引入外部映射以及自动映射,至于其他的映射方式,这里就不再说明了,可以查阅官方文档。
有了关系映射之后,普通的单表查询是无法获取复合映射所需的结果的,这里就必须用到联合查询,然后将联合查询返回的数据列,拆分给不同的对象属性。
1对1嵌套映射
1对1的映射相对简单,我们通过一张图就能理清其基本原理,先来改造一下刚才的查询语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<resultMap id="joinNotCommentsResultMap" type="com.wuwenze.mybatis.example2.entity.Post">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="author"
javaType="com.wuwenze.mybatis.example2.entity.Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
</association>
</resultMap>
<select id="findNotCommentsById" resultMap="joinNotCommentsResultMap">
SELECT
p.id AS id,
p.title AS title,
a.id AS author_id,
a.name AS author_name
FROM post p
LEFT JOIN author a ON a.id = p.author_id
WHERE p.id = #{id}
</select>
|

通过上述SQL语句联合查询,可以得到一条二维平面的结果数据,在结果行中,前两个字段对应的是Post
对象,后两个字段对应的是Author
对象,换句话说,每行都会产生两个Java对象,当然,这取决于ResultMap
的配置。
1对多嵌套映射
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
|
<resultMap id="joinNotAuthorResultMap" type="com.wuwenze.mybatis.example2.entity.Post">
<id property="id" column="id"/>
<result property="title" column="title"/>
<collection property="comments"
ofType="com.wuwenze.mybatis.example2.entity.Comment">
<id property="id" column="comment_id"/>
<result property="body" column="comment_body"/>
<association property="author"
javaType="com.wuwenze.mybatis.example2.entity.Author">
<id property="id" column="comment_author_id"/>
<result property="name" column="comment_author_name"/>
</association>
</collection>
</resultMap>
<select id="findNotAuthorById" resultMap="joinNotAuthorResultMap">
SELECT p.id AS id,
p.title AS title,
c.id AS comment_id,
c.body AS comment_body,
ca.id AS comment_author_id,
ca.name AS comment_author_name
FROM post p
LEFT JOIN comment c ON c.post_id = p.id
LEFT JOIN author ca ON ca.id = c.author_id
WHERE p.id = #{id};
</select>
|
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
|
==> Parameters: 1(Integer)
<== Columns: id, title, comment_id, comment_body, comment_author_id, comment_author_name
<== Row: 1, How to use mybatis?, 1, haha~, 2, Wang
<== Row: 1, How to use mybatis?, 2, I don't know., 3, Tom
<== Total: 2
{
"comments": [
{
"author": {
"name": "Wang",
"id": 2
},
"body": "haha~",
"id": 1
},
{
"author": {
"name": "Tom",
"id": 3
},
"body": "I don't know.",
"id": 2
}
],
"title": "How to use mybatis?",
"id": 1
}
|
上述查询结果中可以得到2条数据,前两个字段对应的是Post
对象,后四个字段对应的则是Comment
字段(List),与一对一不同的是,这两行数据指向的都是同一条Post
数据,因为他们的ID都是同样的。

RowKey创建机制
一对多的关键点在于如何区分数据是否为同一条,换句话说,是基于什么原理对数据进行分组的,在一对多的映射过程中,是基于RowKey
来进行对行数据进行分组的。
默认情况下,RowKey
一般基于配置的<id />
字段,但有时候往往没有配置,这时候它将采用其他所有已经配置的字段,具体规则如下:

结果集的解析流程
这里直接用一对多的情况进行总结,因为一对一其实就是一对多的简化版。

源码分析
所有映射流程的解析都是在DefaultResultSetHandler
当中完成。
首先调用handleRowValues()
方法,进入嵌套映射分支。

handleRowValuesForNestedResultMap()
嵌套结果集解析入口,在这里会遍历结果集中所有行。并为每一行创建一个RowKey
对象。然后调用getRowValue()
获取解析结果对象。最后保存至ResultHandler
中。
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
|
// 暂存区的定义
private final Map<CacheKey, Object> nestedResultObjects = new HashMap<>();
private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
skipRows(resultSet, rowBounds);
Object rowValue = previousRowValue;
// 进行结果行的遍历操作
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
// 为每一行创建一个RowKey对象
final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
// 基于rowkey获取已经在暂存区的数据
Object partialObject = nestedResultObjects.get(rowKey);
// issue #577 && #542
if (mappedStatement.isResultOrdered()) {
if (partialObject == null && rowValue != null) {
nestedResultObjects.clear();
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
// 调用getRowValue获取解析结果对象
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
} else {
rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
if (partialObject == null) {
// 保存对象
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
}
if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
previousRowValue = null;
} else if (rowValue != null) {
previousRowValue = rowValue;
}
}
private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
if (parentMapping != null) {
linkToParents(rs, parentMapping, rowValue);
} else {
callResultHandler(resultHandler, resultContext, rowValue);
}
}
@SuppressWarnings("unchecked" /* because ResultHandler<?> is always ResultHandler<Object>*/)
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
resultContext.nextResultObject(rowValue);
((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}
|
注:调用getRowValue
前会基于RowKey
在暂存区获取已解析的对象,然后作为partialObject
参数发给getRowValue
getRowValue()
该方法最终会基于当前行生成一个解析好对象,具体职责包括,
- 创建对象
- 填充普通属性填充嵌套属性。在解析嵌套属性时会以递归的方式在调用getRowValue获取子对象
- 基于RowKey暂存当前解析对象
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
|
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
final String resultMapId = resultMap.getId();
Object rowValue = partialObject;
// 暂存区存在当前行
if (rowValue != null) {
// 直接填充复合属性
final MetaObject metaObject = configuration.newMetaObject(rowValue);
putAncestor(rowValue, resultMapId);
applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
ancestorObjects.remove(resultMapId);
}
// 暂存区不存在当前行
else {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 创建空对象
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, true)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
// 处理手动映射属性
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
putAncestor(rowValue, resultMapId);
// 关键:处理嵌套映射结果集映射(即:填充复合属性)
foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
ancestorObjects.remove(resultMapId);
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
if (combinedKey != CacheKey.NULL_CACHE_KEY) {
nestedResultObjects.put(combinedKey, rowValue);
}
}
return rowValue;
}
|
applyNestedResultMappings()
解析并填充嵌套结果集映射,遍历所有嵌套映射,然后获取其嵌套ResultMap
,接着创建RowKey 去获取暂存区的值。然后调用getRowValue
获取属性对象,最后填充至父对象。
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
|
private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {
boolean foundValues = false;
// 遍历ResultMapping
for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
final String nestedResultMapId = resultMapping.getNestedResultMapId();
// nestedResultMapId不为空(自动生成的), 则为嵌套映射, 其余的忽略
// com.wuwenze.mybatis.example2.mapper.PostMapper.mapper_resultMap[joinNotAuthorResultMap]_collection[comments]
if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
try {
// 获取配置的属性前缀
final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
// 获取嵌套映射的ResultMap对象
final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
if (resultMapping.getColumnPrefix() == null) {
// try to fill circular reference only when columnPrefix
// is not specified for the nested result map (issue #215)
Object ancestorObject = ancestorObjects.get(nestedResultMapId);
if (ancestorObject != null) {
if (newObject) {
linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
}
continue;
}
}
// 创建当前嵌套映射对象的RowKey,对应的是comments对象
final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
// 将当前嵌套映射对象的RowKey与父类RowKey进行合并
final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
// 从暂存区获取值
Object rowValue = nestedResultObjects.get(combinedKey);
boolean knownValue = rowValue != null;
instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
// 用合并后的RowKey以及嵌套映射的ResultMap对象再走一遍getRowValue流程
rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
if (rowValue != null && !knownValue) {
// 拿到值,使用MetaObject赋值
linkObjects(metaObject, resultMapping, rowValue);
foundValues = true;
}
}
} catch (SQLException e) {
throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'. Cause: " + e, e);
}
}
}
return foundValues;
}
|
如果通过RowKey
能获取到属性对象,它还是会去调用getRowValue
,因为有可能属性下还存在未解析的属性。
循环引用
了解了Mybatis复合属性的映射,那就不得不关注一个问题,如果涉及到循环引用,Mybatis是如何处理的?
举个例子,假设Post
对象与Comment
对象之间相互引用,就像下面这样:

这种情况会导致死循环吗?答案是不会,DefaultResultSetHandler
在解析复合映射之前都会在上下文中填充当前解析对象(使用resultMapId
做为Key)。如果子属性又映射引用了父映射ID,就可以直接获取不需要在去解析父对象。

评论