您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
由 Mybatis 源码畅谈软件设计(五):ResultMap 的循环引用
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
由 Mybatis 源码畅谈软件设计(五):ResultMap 的循环引用
wy****
2024-12-25
IP归属:北京
3浏览
本节我们来了解 Mybatis 是如何处理 ResultMap 的循环引用,它的解决方案非常值得在软件设计中参考。另外作为引申,大家可以了解一下 Spring 是如何解决 Bean 的循环注入的。 以单测 `org.apache.ibatis.submitted.permissions.PermissionsTest#checkNestedResultMapLoop` 为例,它对应表结构和表中的数据为: ```sql 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` 为: ```xml <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 对象的类定义如下: ```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](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-12-15-14-44sK7lfpCOgUk744sX.png) 由图示可知,Mybatis 在处理循环引用时,会根据引用关系创建最外层对象,每遇到新的引用,都会创建新的对象,并将这些对象“存”起来,当遇到现有对象需要被引用时,则会从“缓存”中取,不断地回归处理引用关系,这和算法中“递归”的思想一致,接下来我们看一下源码中是如何处理的,我们直接看 `org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForNestedResultMap` 方法,它是处理循环引用的入口: ```java 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](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-12-15-14-44C44398SdUWOYot6sS.png) 可以发现 `resource1` 的 `principals` 字段会对应多个 `Principal` 对象,那么在解析完数据库中第一行 `resource1` 的数据时,它所需要的 `Principal` 集合的一对多关系并没有完成赋值,会将其缓存起来,那么在处理数据库中第二行 `resource1` 的数据时,需要将它添加到一对多集合中,这时候便会从缓存 `Map<CacheKey, Object> nestedResultObjects` 获取出来处理第一行的数据,因为第二行数据的 `name` 同样为 `resource1` 所以能通过 `CacheKey` 获取到已完成处理的第一行数据对应的对象,这样便能完成一对多关系的封装。 **第二点**:`DefaultResultSetHandler#getRowValue` 方法,它是处理循环引用,将数据库中数据处理成 Java 对象的核心方法,如下所示: ```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` 方法是用于处理循环引用关系的方法: ```java 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 的循环引用并不复杂,在本节中我们并没有深入源码的细节,更多关注的是解决循环引用的方法,即 **递归 + 缓存** 的解决方案,建议大家执行对应单测来熟悉流程并了解相关细节。
上一篇:设计模式之代理模式:武器附魔之道
下一篇:由 Mybatis 源码畅谈软件设计(七):从根上理解 Mybatis 一级缓存
wy****
文章数
32
阅读量
2526
作者其他文章
01
高性能MySQL实战(一):表结构
最近因需求改动新增了一些数据库表,但是在定义表结构时,具体列属性的选择有些不知其所以然,索引的添加也有遗漏和不规范的地方,所以我打算为创建一个高性能表的过程以实战的形式写一个专题,以此来学习和巩固这些知识。1. 实战我使用的 MySQL 版本是 5.7,建表 DDL 语句如下所示:根据需求创建 接口调用日志 数据库表,请大家浏览具体字段的属性信息,它们有不少能够优化的点。CREATE TABLE
01
分布式服务高可用实现:复制
1. 为什么需要复制我们可以考虑如下问题:当数据量、读取或写入负载已经超过了当前服务器的处理能力,如何实现负载均衡?希望在单台服务器出现故障时仍能继续工作,这该如何实现?当服务的用户遍布全球,并希望他们访问服务时不会有较大的延迟,怎么才能统一用户的交互体验?这些问题其实都能通过 “复制” 来解决:复制,即在不同的节点上保存相同的副本,提供数据冗余。如果一些节点不可用,剩余的节点仍然可以提供数据服务
01
高性能MySQL实战(三):性能优化
这篇主要介绍对慢 SQL 优化的一些手段,而在讲解具体的优化措施之前,我想先对 EXPLAIN 进行介绍,它是我们在分析查询时必要的操作,理解了它输出结果的内容更有利于我们优化 SQL。为了方便大家的阅读,在下文中规定类似 key1 的表示二级索引,key_part1 表示联合索引的第一部分,unique_key1 则表示唯一二级索引,primary_key 表示主键索引。高性能MySQL实战(一
01
从2PC和容错共识算法讨论zookeeper中的Create请求
最近在读《数据密集型应用系统设计》,其中谈到了zookeeper对容错共识算法的应用。这让我想到之前参考的zookeeper学习资料中,误将容错共识算法写成了2PC(两阶段提交协议),所以准备以此文对共识算法和2PC做梳理和区分,也希望它能帮助像我一样对这两者有误解的同学。1. 2PC(两阶段提交协议)两阶段提交 (two-phase commit) 协议是一种用于实现 跨多个节点的原子事务(分布
wy****
文章数
32
阅读量
2526
作者其他文章
01
高性能MySQL实战(一):表结构
01
分布式服务高可用实现:复制
01
高性能MySQL实战(三):性能优化
01
从2PC和容错共识算法讨论zookeeper中的Create请求
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号