由 Mybatis 源码畅谈软件设计(五):ResultMap 的循环引用

京东云开发者
• 阅读 66

作者:京东保险 王奕龙

本节我们来了解 Mybatis 是如何处理 ResultMap 的循环引用,它的解决方案非常值得在软件设计中参考。另外作为引申,大家可以了解一下 Spring 是如何解决 Bean 的循环注入的。

以单测 org.apache.ibatis.submitted.permissions.PermissionsTest#checkNestedResultMapLoop 为例,它对应表结构和表中的数据为:

create table permissions (
  resourceName varchar(20),
  principalName varchar(20),
  permission varchar(20)
);

insert into permissions values ('resource1', 'user1', 'read');
insert into permissions values ('resource1', 'user2', 'read');
insert into permissions values ('resource1', 'user1', 'create');
insert into permissions values ('resource2', 'user1', 'delete');
insert into permissions values ('resource2', 'user1', 'update');

在 Mapper 中定义的循环引用的 ResultMap 为:

<mapper namespace="org.apache.ibatis.submitted.permissions.PermissionsMapper">

    <resultMap id="resourceResults" type="Resource">
        <id property="name" column="resourceName" />
        <collection property="principals" resultMap="principalResults" />
    </resultMap>

    <resultMap id="principalResults" type="Principal">
        <id property="principalName" column="principalName" />
        <collection property="permissions" resultMap="permissionResults" />
    </resultMap>

    <resultMap id="permissionResults" type="Permission">
        <result property="permission" column="permission" />
        <association property="resource" resultMap="resourceResults" />
    </resultMap>

    <!-- ... -->
</mapper>

resourceResults 引用 principalResults 引用 permissionResults 引用 resourceResults,构建成了循环引用。

将数据库中数据映射为 Java 对象的类定义如下:

public class Resource {

    private String name;

    private List<Principal> principals = new ArrayList<>();

}

public class Principal {

    private String principalName;

    private List<Permission> permissions = new ArrayList<>();

}

public class Permission {

    private String permission;

    private Resource resource;
}

为了方便大家理解,在看源码前,先给大家图示下循环引用构造结果对象的流程:

由 Mybatis 源码畅谈软件设计(五):ResultMap 的循环引用

由图示可知,Mybatis 在处理循环引用时,会根据引用关系创建最外层对象,每遇到新的引用,都会创建新的对象,并将这些对象“存”起来,当遇到现有对象需要被引用时,则会从“缓存”中取,不断地回归处理引用关系,这和算法中“递归”的思想一致,接下来我们看一下源码中是如何处理的,我们直接看 org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForNestedResultMap 方法,它是处理循环引用的入口:

public class DefaultResultSetHandler implements ResultSetHandler {
    // ...
    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);
            // 根据ID字段名和值(或其他字段名和值,不包括循环引用字段)信息创建缓存 key,这样同一个字段的同一个值就对应了一个缓存对象,避免重复创建对象
            // 这样,在做一对多或多对一时,便能根据 key 值获取到所属对象
            final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
            // 循环引用对象缓存中获取对象;partial 的释义为 adj.部分的,如此命名表示该对象中一对多或多对一关系未被处理完成
            Object partialObject = nestedResultObjects.get(rowKey);

            if (mappedStatement.isResultOrdered()) {
                if (partialObject == null && rowValue != null) {
                    nestedResultObjects.clear();
                    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
                }
                rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
            } else {
                // 获取该行数据库对应的 Java 对象
                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;
        }
    }
}

在这个方法中需要特别关注两个点:

第一点:缓存 CacheKey rowKey key的创建规则和缓存 Map<CacheKey, Object> nestedResultObjects。它们的作用是什么呢?CacheKey 会根据字段和字段值完成创建,比如以 Resource 中字段 name 值为 resource1 的数据为例,虽然在数据库中有多行相同的 name 值数据(文章开篇示例 SQL 中向 permissions 表中插入多条 name 值相同的数据),但是它们会对应到同一个 CacheKey 对象,那么这样在解决 resourceResults 中定义的 collection 标签 的一对多关系时,能直接获取到对应的 Resource 对象,并向其中表示一对多关系的集合中添加值。以我们的样例数据为例,查询完毕后的对象如下所示:

由 Mybatis 源码畅谈软件设计(五):ResultMap 的循环引用

可以发现 resource1principals 字段会对应多个 Principal 对象,那么在解析完数据库中第一行 resource1 的数据时,它所需要的 Principal 集合的一对多关系并没有完成赋值,会将其缓存起来,那么在处理数据库中第二行 resource1 的数据时,需要将它添加到一对多集合中,这时候便会从缓存 Map<CacheKey, Object> nestedResultObjects 获取出来处理第一行的数据,因为第二行数据的 name 同样为 resource1 所以能通过 CacheKey 获取到已完成处理的第一行数据对应的对象,这样便能完成一对多关系的封装。

第二点DefaultResultSetHandler#getRowValue 方法,它是处理循环引用,将数据库中数据处理成 Java 对象的核心方法,如下所示:

public class DefaultResultSetHandler implements ResultSetHandler {

    private final Map<String, Object> ancestorObjects = new HashMap<>();

    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 时,表示数据库包含多行相同键值数据,需要处理它们的聚合关系,一对多or多对一
            final MetaObject metaObject = configuration.newMetaObject(rowValue);
            ancestorObjects.put(resultMapId, rowValue);
            // 处理循环引用的映射关系
            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;
                }
                // 根据 result mapping 中配置的字段和数据库列的映射关系,从 resultSet 中取值后封装给 metaObject
                foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
                // 添加到 ancestor 缓存中,用于封装循环引用对象;ancestor 祖先,原型
                ancestorObjects.put(resultMapId, rowValue);
                // 处理循环引用的映射关系
                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;
    }
}

其中有两个分支,分别为 partialObject 是否为空的情况,为空时会创建对应的结果对象,并为非循环引用的字段赋值(applyPropertyMappings 方法),不为空时它便是我们在我们上述的 nestedResultObjects 缓存中获取到了对象,来处理它的聚合关系。该方法中使用到的 Map<String, Object> ancestorObjects 缓存需要强调下,它是用来 处理循环引用关系的缓存。回到文章开头的流程图示,在第 4 步中,要获取 Resource 对象赋值便是从 ancestorObjects 缓存中获取的,Resource 对象先被创建后并置于缓存中,当后续有对象引用它时,直接在缓存中获取,避免重复创建,解决循环引用的问题。

其中 applyNestedResultMappings 方法是用于处理循环引用关系的方法:

public class DefaultResultSetHandler implements ResultSetHandler {

    private final Map<CacheKey, Object> nestedResultObjects = new HashMap<>();

    private final Map<String, Object> ancestorObjects = new HashMap<>();

    private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
                                              String parentPrefix, CacheKey parentRowKey, boolean newObject) {
        boolean foundValues = false;
        for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
            final String nestedResultMapId = resultMapping.getNestedResultMapId();
            if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
                try {
                    final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
                    final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
                    if (resultMapping.getColumnPrefix() == null) {
                        // 为未声明列前缀的 result_mapping 封装循环引用对象
                        Object ancestorObject = ancestorObjects.get(nestedResultMapId);
                        if (ancestorObject != null) {
                            if (newObject) {
                                linkObjects(metaObject, resultMapping, ancestorObject);
                            }
                            continue;
                        }
                    }
                    // 同样创建缓存 KEY,并从循环应用缓存中获取已经创建但可能未完成一对多和多对一关系的对象
                    final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
                    final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
                    Object rowValue = nestedResultObjects.get(combinedKey);
                    boolean knownValue = rowValue != null;
                    instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
                    if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
                        // 获取该行数据
                        rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
                        if (rowValue != null && !knownValue) {
                            // 封装到结果对象中
                            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;
    }

    private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
        final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
        // 如果是一对多关系,则添加到对应集合中
        if (collectionProperty != null) {
            final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
            targetMetaObject.add(rowValue);
        } else {
            // 否则直接为对应字段赋值
            metaObject.setValue(resultMapping.getProperty(), rowValue);
        }
    }
}

值得关注的是该方法中也调用了 getRowValue 方法,这样便形成了 递归调用,这也是解决循环引用问题的关键。另一个需要关注的是其中的 linkObjects 封装结果的方法,如果是一对多关系,它会向集合中进行添加,否则便直接为对象赋值。

ResultMap 的循环引用并不复杂,在本节中我们并没有深入源码的细节,更多关注的是解决循环引用的方法,即 递归 + 缓存 的解决方案,建议大家执行对应单测来熟悉流程并了解相关细节。

点赞
收藏
评论区
推荐文章
0源码基础学习Spring源码系列(二)——Spring如何解决循环依赖
本篇文章适用于0基础学习spring源码,文章重点解析spring如何解决循环依赖,并从解决循环依赖过程、三级缓存在循环依赖中的作用、解决代理对象的问题、二级缓存、初始化几个维度出发,解析spring源码。
Stella981 Stella981
3年前
Python对象的循环引用问题
\toc\Python对象循环引用我们来介绍一下Python是采用何种途径解决循环引用问题的。循环引用垃圾回收算法!(https://oscimg.oschina.net/oscnet/3786a746224d888c57cd06086dc52bff3b0.png)上图中,表示的
Stella981 Stella981
3年前
Spring core 源码分析
    上节提到了在AbstractApplicationContext调用refresh方法里,初始化所有BeanDefinitions后,遍历所有BeanDefinitionNames后,循环调用BeanFactory的getBean(name)方法,实例化所有容器Bean对象(非lasyinit)。GetBean做了什么?循环引用如何处理
京东云开发者 京东云开发者
2个月前
分布式服务高可用实现:复制
作者:京东保险王奕龙1.为什么需要复制我们可以考虑如下问题:1.当数据量、读取或写入负载已经超过了当前服务器的处理能力,如何实现负载均衡?2.希望在单台服务器出现故障时仍能继续工作,这该如何实现?3.当服务的用户遍布全球,并希望他们访问服务时不会有较大的延
京东云开发者 京东云开发者
2星期前
由 Mybatis 源码畅谈软件设计(二):MappedStatement 和 SqlSource
作者:京东保险王奕龙本节我们来介绍org.apache.ibatis.mapping.MappedStatement(映射SQL语句声明的类),它是MyBatis框架中的一个核心类,也是向后不断学习Mybatis源码的基础。在这部分源码中,最值得关注的设计
由 Mybatis 源码畅谈软件设计(四):动态 SQL 执行流程
作者:京东保险王奕龙本节我们探究动态SQL的执行流程,由于在前一节我们已经对各个组件进行了详细介绍,所以本节不再赘述相关内容,在本节中主要强调静态SQL和动态SQL执行的不同之处。在这个过程中,SqlNode相关实现值得关注,它为动态SQL标签都定义了专用
由 Mybatis 源码畅谈软件设计(七):从根上理解 Mybatis 一级缓存
作者:京东保险王奕龙本篇我们来讲一级缓存,重点关注它的实现原理:何时生效、生效范围和何时失效,在未来设计缓存使用时,提供一些借鉴和参考。1.准备工作定义实体publicclassDepartmentpublicDepartment(Stringid)thi
由 Mybatis 源码畅谈软件设计(九):“能用就行” 其实远远不够
作者:京东保险王奕龙到本节Mybatis源码中核心逻辑基本已经介绍完了,在这里我想借助Mybatis其他部分源码来介绍一些我认为在编程中能最快提高编码质量的小方法,它们可能比较细碎,希望能对大家有所启发。关于方法的长度和方法拆分之前我在读完《代码整洁之道》
从源码层面深度剖析Spring循环依赖 | 京东云技术团队
本文从源码层面介绍了Spring如何创建bean、如何解决循环依赖,同时也介绍了不能解决哪些循环依赖,同时在文章的最后解决循环依赖报错的几个方法
京东云开发者 京东云开发者
2个月前
「软件设计哲学」于延保代码改造中的实践
作者:京东保险王奕龙本文主要给大家分享软件设计中的两个理念,为什么我称软件设计是“理念”而不是“方法”或“原则”呢?这个想法主要受《Aphilosophyofsoftwaredesign》的影响,它将软件设计称为“哲学”,而哲学本身没有严格的定论,同样地,