首页 > 编程语言 >由 Mybatis 源码畅谈软件设计(五):ResultMap 的循环引用

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

时间:2024-12-31 11:43:35浏览次数:1  
标签:缓存 rsw ResultMap 源码 引用 rowValue Mybatis null final

作者:京东保险 王奕龙

本节我们来了解 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;
}

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

nestedResultMap.drawio.png

由图示可知,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 对象,并向其中表示一对多关系的集合中添加值。以我们的样例数据为例,查询完毕后的对象如下所示:

resultMap结果对象.png

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

标签:缓存,rsw,ResultMap,源码,引用,rowValue,Mybatis,null,final
From: https://www.cnblogs.com/Jcloud/p/18643641

相关文章

  • 在 MyBatis-Plus 中使用 IN 语法
    在MyBatis-Plus中使用IN语法在MyBatis-Plus中使用IN​语法可以通过以下几种方式实现:1.使用QueryWrapper​的in​方法​QueryWrapper​是MyBatis-Plus提供的查询条件构造器,可以使用in​方法来构建IN​查询。importcom.baomidou.mybatisplus.core.conditio......
  • 由 Mybatis 源码畅谈软件设计(七):从根上理解 Mybatis 一级缓存
    作者:京东保险王奕龙本篇我们来讲一级缓存,重点关注它的实现原理:何时生效、生效范围和何时失效,在未来设计缓存使用时,提供一些借鉴和参考。1.准备工作定义实体publicclassDepartment{publicDepartment(Stringid){this.id=id;}privateStri......
  • DVWA靶场Command Injection(命令注入) 漏洞low(低),medium(中等),high(高)所有级别通关
    命令注入命令注入漏洞是一种安全漏洞,攻击者可以通过向应用程序输入恶意命令,诱使系统执行这些命令,从而达到未授权访问、数据篡改、系统控制等目的。该漏洞通常出现在应用程序未对用户输入进行充分验证和清理时常见管道符:;前面的执行完执行后面的|上一条命令的输出,作......
  • 你有看过vue的nextTick源码吗?
    是的,我了解过Vue的$nextTick源码。$nextTick是Vue中一个非常重要的机制,它允许我们在DOM更新后执行某些操作,确保我们可以获取到更新后的DOM状态。下面我将对$nextTick的源码进行简要的解析和归纳:$nextTick的基本原理:Vue在更新DOM时是异步执行的。当数据发生变化时,Vue会将这个......
  • 高校班级同学录网站设计与实现+jsp源码+论文
    项目简介高校班级同学录网站是一个综合性信息管理平台,旨在提高高校班级同学录信息处理的效率和安全性。该系统通过精心设计的功能模块,满足了不同用户角色的需求,包括管理员、学院主管、学校主管、校友以及班级校友。系统核心功能涵盖了班级校友管理、学院主管管理、学校主管管......
  • 基于vue的视频播放器的设计与实现+vue源码+论文
    项目简介视频播放器系统是一个多功能的在线视频管理平台,它通过精心设计的功能模块,满足了管理员和用户的不同需求。系统主要分为管理员和用户两大角色,各自拥有独立的功能权限。管理员模块具备强大的后台管理功能,包括视频信息管理、视频留言管理、用户管理和论坛管理等,能够对视......
  • ssm物流信息管理4bf18程序+源码+数据库+调试部署+开发环境
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、研究背景与意义随着信息技术的快速发展,物流信息管理在现代物流体系中扮演着至关重要的角色。高效的物流信息管理不仅能提升物流效率,还能优化资......
  • ssm网络商城系统56077(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、项目背景随着互联网技术的普及和电子商务的快速发展,网络购物已成为人们日常生活的重要组成部分。网络商城系统作为电子商务的核心平台,其重要性......
  • 2024秋季学期 数据结构期末实验报告(无源码版)
    前言这玩意在我看来,p用没有,纯浪费时间,但是沟槽的课有这个要求那我只能花了一点点时间水水了。如果对里面的内容感兴趣(应该不会有人没事来看这种sb玩意吧),可以私信我~实验一疏松多项式1.1问题描述使用链表结构储存疏松多项式并实现以下功能:输入并创建多项式(按指数升序或降序......
  • flask框架人力资源管理系统的核心设计与实现毕设源码+论文
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、选题背景关于人力资源管理系统的研究,现有研究多聚焦于整体系统框架的构建与功能的常规罗列。在国内外,许多大型企业已经应用较为成熟的人力资源......