MyBatis <resultMap>中的<id>的作用到底是什么?

在学习MyBatis的时候,我查阅了【深入浅出MyBatis系列三】Mapper映射文件配置 - 陶邦仁的个人空间 - OSCHINA这篇文章,里面讲到了<resultMap>中的<id>,但是并没有详细地解释它的作用,于是就有了这篇文章。

准备

我们先创建teacherstudent表,一个老师可以对应多个学生。

create table teacher
(
    id   int,
    name varchar(100)
);

create table student
(
    id         int,
    name       varchar(100),
    teacher_id int
);

初始化数据

-- 注意两个老师的名字是一样的
INSERT INTO guiderank_server.teacher (id, name)
VALUES (1, 'Jason'),
       (2, 'Jason');

-- id为1的老师关联2个学生,id为2的老师关联4个学生
INSERT INTO guiderank_server.student (id, name, teacher_id)
VALUES (1, 's_1', 1),
       (2, 's_2', 1),
       (3, 's_3', 2),
       (4, 's_4', 2),
       (5, 's_5', 2),
       (6, 's_6', 2);

Teacher

@Data
public class Teacher {

    private Integer id;

    private String name;

    private List<Student> students;
}

Student

@Data
public class Student {

    private Integer id;

    private String name;
}

我们希望将

select t.id as t_id, t.name as t_name, s.id as s_id, s.name as s_name
from teacher t
         join student s on t.id = s.teacher_id;

-- 执行结果为:
+------+--------+------+--------+
| t_id | t_name | s_id | s_name |
+------+--------+------+--------+
|    1 | Jason  |    1 | s_1    |
|    1 | Jason  |    2 | s_2    |
|    2 | Jason  |    3 | s_3    |
|    2 | Jason  |    4 | s_4    |
|    2 | Jason  |    5 | s_5    |
|    2 | Jason  |    6 | s_6    |
+------+--------+------+--------+

的执行结果转变为List<Teacher> teachers,期望的结果是:teachers有两个元素,分别对应id为1和2的老师,并且id为1的老师的students的大小为2,id为2的老师的students的大小为4。

<id>是用来干嘛的?

<id>就是用来解决上面所说的问题的,也就是说<id>可以用来实现聚集。为了将查询结果转变为我们期望的List<Teacher> teachers,我们需要定义如下的<resultMap>。

<resultMap id="resultMap" type="Teacher">
    <id column="t_id" property="id"/>
    <result column="t_name" property="name"/>
    <collection property="students" ofType="Student">
        <result column="s_id" property="id"/>
        <result column="s_name" property="name"/>
    </collection>
</resultMap>

<id> VS <result>

<id>和<result>有什么区别呢?我们来做一下实验吧。

  1. <id column="t_id" property="id"/>改为<result column="t_id" property="id"/>

结果还是:teachers有两个元素,分别对应id为1和2的老师,并且id为1的老师的students的大小为2,id为2的老师的students的大小为4。

  1. <result column="t_name" property="name"/>改为<id column="t_name" property="name"/>并将其与<id column="t_id" property="id"/>调换位置。

结果变了,现在teachers只有一个元素,这个唯一的老师的id为1,它拥有六个学生。

为什么会出现这样的情况呢?请看接下来的原理分析。

原理

SQL的查询结果是这样子的。

+------+--------+------+--------+
| t_id | t_name | s_id | s_name |
+------+--------+------+--------+
|    1 | Jason  |    1 | s_1    |
|    1 | Jason  |    2 | s_2    |
|    2 | Jason  |    3 | s_3    |
|    2 | Jason  |    4 | s_4    |
|    2 | Jason  |    5 | s_5    |
|    2 | Jason  |    6 | s_6    |
+------+--------+------+--------+

那么MyBatis是怎么将它转变为List<Teacher> teachers的呢?而且为什么会有上面所描述的那些种种表现呢?这一切都跟ResultSetHandler.handleResultSets(Statement stmt)有关,这个方法是用来处理结果集的。

在看代码之前,先讲一下大概的处理过程。其实可以将整个过程理解为rowStream.collect(Collectors.groupingBy(row -> row.getTeacherId, Collectors.toList())),也就是说使用teacherId进行分组,每个组的学生形成teacher.students。

现在你应该明白上面所说的那些现象的原因吧。1. 将<id column="t_id" property="id"/>改为<result column="t_id" property="id"/>,结果没变化。这是因为在没有配置<id>时,MyBatis默认将所有<result>作为一个分组的依据,也就是t_id和t_name都相同的行为一组。2. 将<result column="t_name" property="name"/>改为<id column="t_name" property="name"/>并将其与<id column="t_id" property="id"/>调换位置,结果只有一个元素,这是因为虽然存在1和2这两个t_id,但是t_name都是一样的,而此时MyBatis是使用t_name作为分组的依据。

现在你应该明白了大概的思路,那我们看看具体的代码吧。

  1. DefaultResultSetHandler.handleResultSets(Statement stmt)
//
// HANDLE RESULT SETS
//
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>(); // 存储转换结果,每个元素为resultSet的转换结果。此例中,multipleResults只有一个元素,就是我们所说的`List<Teacher> teachers`

    int resultSetCount = 0; // 语句可能有多个resultSet,每个resultSet都需要被对应的resultMap转换。这里初始化resultSetCount为0
    ResultSetWrapper rsw = getFirstResultSet(stmt); // 获取第一个结果集

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) { // 循环,使用对应的resultMap处理resultSet
        ResultMap resultMap = resultMaps.get(resultSetCount); // 获取对应的resultMap
        handleResultSet(rsw, resultMap, multipleResults, null); // 处理结果集,并将处理结果加进multipleResults
        rsw = getNextResultSet(stmt); // 将rsw设置为下一个结果集
        cleanUpAfterHandlingResultSet();
        resultSetCount++; // 改变resultSetCount
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
        while (rsw != null && resultSetCount < resultSets.length) {
            ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
            if (parentMapping != null) {
                String nestedResultMapId = parentMapping.getNestedResultMapId();
                ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
                handleResultSet(rsw, resultMap, null, parentMapping);
            }
            rsw = getNextResultSet(stmt);
            cleanUpAfterHandlingResultSet();
            resultSetCount++;
        }
    }

    return collapseSingleResultList(multipleResults); // 如果multipleResults只有一个元素。直接返回那个元素,否则返回multipleResults
}
  1. DefaultResultSetHandler.handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping)
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
        if (parentMapping != null) {
            handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
        } else {
            if (resultHandler == null) {
                DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
                handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null); // 处理结果集,并将转换结果存储在defaultResultHandler
                multipleResults.add(defaultResultHandler.getResultList()); // 将defaultResultHandler中的转换结果加进multipleResults
            } else {
                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
            }
        }
    } finally {
        // issue #228 (close resultsets)
        closeResultSet(rsw.getResultSet());
    }
}
  1. DefaultResultSetHandler.handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
//
// HANDLE ROWS FOR SIMPLE RESULTMAP
//

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) { // 含有内嵌resultMap
        ensureNoRowBounds();
        checkResultHandler();
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); // 处理含有内嵌resultMap的结果集,并将转换结果存储在resultHandler
    } else {
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}
  1. DefaultResultSetHandler.handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
//
// HANDLE NESTED RESULT MAPS
//

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);
        final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null); // 它的值类似于`-1885500083:-515674352:BookMapper.resultMap:t_id:1`,注意后面的`t_id:1`,这就是id为1的老师的标识呀
        Object partialObject = nestedResultObjects.get(rowKey); // 获取id为1的teacher对象,为什么叫partialObject呢?因为这个对象可能不是完整的,它可能为null,也就是说第一次碰到这个老师,也可能是关联了一个或多个学生的老师
        // issue #577 && #542
        if (mappedStatement.isResultOrdered()) {
            if (partialObject == null && rowValue != null) {
                nestedResultObjects.clear();
                storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
            }
            rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
        } else {
            rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); // 如果partialObject为null,将一行数据转换为一个teacher,这个teacher的students只有一个元素,并且会修改nestedResultObjects,用来记录teacherId和teacher对象的关联关系;如果partialObject不为null,不再创建teacher,直接将一行数据对应的学生加进已有的students
            if (partialObject == null) { // 第一次碰到这个老师
                storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); // 将这个老师加进在resultHandler
            }
        }
    }
    if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
        storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
        previousRowValue = null;
    } else if (rowValue != null) {
        previousRowValue = rowValue;
    }
}
  1. DefaultResultSetHandler.getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject)
//
// GET VALUE FROM ROW FOR NESTED RESULT MAP
//

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) { // rowValue不为null,也就是说partialObject不为null。可以理解为已经存在这个老师
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        putAncestor(rowValue, resultMapId);
        applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false); // 直接处理内嵌结果映射,不再创建老师,而是直接往老师的学生列表加入一个学生(其实加入学生也会判断是否重复,本文不讲解,有兴趣自己去看看😬)
        ancestorObjects.remove(resultMapId);
    } else { // rowValue为null,也就是说partialObject为null。可以理解为还不存在这个老师
        final ResultLoaderMap lazyLoader = new ResultLoaderMap();
        rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); // 创建一个结果对象,也就是一个teacher
        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; // 处理属性映射,也就是设置teacher的id和name
            putAncestor(rowValue, resultMapId);
            foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues; // 处理内嵌结果映射,也就是设置teacher的students
            ancestorObjects.remove(resultMapId);
            foundValues = lazyLoader.size() > 0 || foundValues;
            rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
        }
        if (combinedKey != CacheKey.NULL_CACHE_KEY) {
            nestedResultObjects.put(combinedKey, rowValue); // 将combinedKey和rowValue关联起来,可以理解为将老师的id跟老师对象关联起来
        }
    }
    return rowValue;
}

现在你应该清楚之前所说的那些现象背后的原因吧。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351