作者:京东保险 王奕龙
本节我们来了解 Mybatis 是如何处理 ResultMap 的循环引用,它的解决方案非常值得在软件设计中参考。另外作为引申,大家可以了解一下 Spring 是如何解决 Bean 的循环注入的。
以单测 org.apache.ibatis.submitted.permissions.PermissionsTest#checkNestedResultMapLoop 为例,它对应表结构和表中的数据为:
1create table permissions ( 2 resourceName varchar(20), 3 principalName varchar(20), 4 permission varchar(20) 5); 6 7insert into permissions values ('resource1', 'user1', 'read'); 8insert into permissions values ('resource1', 'user2', 'read'); 9insert into permissions values ('resource1', 'user1', 'create'); 10insert into permissions values ('resource2', 'user1', 'delete'); 11insert into permissions values ('resource2', 'user1', 'update');
在 Mapper 中定义的循环引用的 ResultMap 为:
1<mapper namespace="org.apache.ibatis.submitted.permissions.PermissionsMapper"> 2 3 <resultMap id="resourceResults" type="Resource"> 4 <id property="name" column="resourceName" /> 5 <collection property="principals" resultMap="principalResults" /> 6 </resultMap> 7 8 <resultMap id="principalResults" type="Principal"> 9 <id property="principalName" column="principalName" /> 10 <collection property="permissions" resultMap="permissionResults" /> 11 </resultMap> 12 13 <resultMap id="permissionResults" type="Permission"> 14 <result property="permission" column="permission" /> 15 <association property="resource" resultMap="resourceResults" /> 16 </resultMap> 17 18 <!-- ... --> 19</mapper>
resourceResults 引用 principalResults 引用 permissionResults 引用 resourceResults,构建成了循环引用。
将数据库中数据映射为 Java 对象的类定义如下:
1public class Resource { 2 3 private String name; 4 5 private List<Principal> principals = new ArrayList<>(); 6 7} 8 9public class Principal { 10 11 private String principalName; 12 13 private List<Permission> permissions = new ArrayList<>(); 14 15} 16 17public class Permission { 18 19 private String permission; 20 21 private Resource resource; 22}
为了方便大家理解,在看源码前,先给大家图示下循环引用构造结果对象的流程:

由图示可知,Mybatis 在处理循环引用时,会根据引用关系创建最外层对象,每遇到新的引用,都会创建新的对象,并将这些对象“存”起来,当遇到现有对象需要被引用时,则会从“缓存”中取,不断地回归处理引用关系,这和算法中“递归”的思想一致,接下来我们看一下源码中是如何处理的,我们直接看 org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForNestedResultMap 方法,它是处理循环引用的入口:
1public class DefaultResultSetHandler implements ResultSetHandler { 2 // ... 3 private final Map<CacheKey, Object> nestedResultObjects = new HashMap<>(); 4 5 private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, 6 ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { 7 final DefaultResultContext<Object> resultContext = new DefaultResultContext<>(); 8 ResultSet resultSet = rsw.getResultSet(); 9 skipRows(resultSet, rowBounds); 10 Object rowValue = previousRowValue; 11 while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { 12 final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); 13 // 根据ID字段名和值(或其他字段名和值,不包括循环引用字段)信息创建缓存 key,这样同一个字段的同一个值就对应了一个缓存对象,避免重复创建对象 14 // 这样,在做一对多或多对一时,便能根据 key 值获取到所属对象 15 final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null); 16 // 循环引用对象缓存中获取对象;partial 的释义为 adj.部分的,如此命名表示该对象中一对多或多对一关系未被处理完成 17 Object partialObject = nestedResultObjects.get(rowKey); 18 19 if (mappedStatement.isResultOrdered()) { 20 if (partialObject == null && rowValue != null) { 21 nestedResultObjects.clear(); 22 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); 23 } 24 rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); 25 } else { 26 // 获取该行数据库对应的 Java 对象 27 rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); 28 if (partialObject == null) { 29 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); 30 } 31 } 32 } 33 if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) { 34 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); 35 previousRowValue = null; 36 } else if (rowValue != null) { 37 previousRowValue = rowValue; 38 } 39 } 40}
在这个方法中需要特别关注两个点:
第一点:缓存 CacheKey rowKey key的创建规则和缓存 Map<CacheKey, Object> nestedResultObjects。它们的作用是什么呢?CacheKey 会根据字段和字段值完成创建,比如以 Resource 中字段 name 值为 resource1 的数据为例,虽然在数据库中有多行相同的 name 值数据(文章开篇示例 SQL 中向 permissions 表中插入多条 name 值相同的数据),但是它们会对应到同一个 CacheKey 对象,那么这样在解决 resourceResults 中定义的 collection 标签 的一对多关系时,能直接获取到对应的 Resource 对象,并向其中表示一对多关系的集合中添加值。以我们的样例数据为例,查询完毕后的对象如下所示:

可以发现 resource1 的 principals 字段会对应多个 Principal 对象,那么在解析完数据库中第一行 resource1 的数据时,它所需要的 Principal 集合的一对多关系并没有完成赋值,会将其缓存起来,那么在处理数据库中第二行 resource1 的数据时,需要将它添加到一对多集合中,这时候便会从缓存 Map<CacheKey, Object> nestedResultObjects 获取出来处理第一行的数据,因为第二行数据的 name 同样为 resource1 所以能通过 CacheKey 获取到已完成处理的第一行数据对应的对象,这样便能完成一对多关系的封装。
第二点:DefaultResultSetHandler#getRowValue 方法,它是处理循环引用,将数据库中数据处理成 Java 对象的核心方法,如下所示:
1public class DefaultResultSetHandler implements ResultSetHandler { 2 3 private final Map<String, Object> ancestorObjects = new HashMap<>(); 4 5 private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, 6 Object partialObject) throws SQLException { 7 final String resultMapId = resultMap.getId(); 8 Object rowValue = partialObject; 9 if (rowValue != null) { 10 // rowValue 不为 null 时,表示数据库包含多行相同键值数据,需要处理它们的聚合关系,一对多or多对一 11 final MetaObject metaObject = configuration.newMetaObject(rowValue); 12 ancestorObjects.put(resultMapId, rowValue); 13 // 处理循环引用的映射关系 14 applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false); 15 ancestorObjects.remove(resultMapId); 16 } else { 17 final ResultLoaderMap lazyLoader = new ResultLoaderMap(); 18 // 创建未赋值的结果对象 19 rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); 20 if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { 21 final MetaObject metaObject = configuration.newMetaObject(rowValue); 22 boolean foundValues = this.useConstructorMappings; 23 if (shouldApplyAutomaticMappings(resultMap, true)) { 24 foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; 25 } 26 // 根据 result mapping 中配置的字段和数据库列的映射关系,从 resultSet 中取值后封装给 metaObject 27 foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; 28 // 添加到 ancestor 缓存中,用于封装循环引用对象;ancestor 祖先,原型 29 ancestorObjects.put(resultMapId, rowValue); 30 // 处理循环引用的映射关系 31 foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) 32 || foundValues; 33 ancestorObjects.remove(resultMapId); 34 foundValues = lazyLoader.size() > 0 || foundValues; 35 rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null; 36 } 37 if (combinedKey != CacheKey.NULL_CACHE_KEY) { 38 nestedResultObjects.put(combinedKey, rowValue); 39 } 40 } 41 return rowValue; 42 } 43}
其中有两个分支,分别为 partialObject 是否为空的情况,为空时会创建对应的结果对象,并为非循环引用的字段赋值(applyPropertyMappings 方法),不为空时它便是我们在我们上述的 nestedResultObjects 缓存中获取到了对象,来处理它的聚合关系。该方法中使用到的 Map<String, Object> ancestorObjects 缓存需要强调下,它是用来 处理循环引用关系的缓存。回到文章开头的流程图示,在第 4 步中,要获取 Resource 对象赋值便是从 ancestorObjects 缓存中获取的,Resource 对象先被创建后并置于缓存中,当后续有对象引用它时,直接在缓存中获取,避免重复创建,解决循环引用的问题。
其中 applyNestedResultMappings 方法是用于处理循环引用关系的方法:
1public class DefaultResultSetHandler implements ResultSetHandler { 2 3 private final Map<CacheKey, Object> nestedResultObjects = new HashMap<>(); 4 5 private final Map<String, Object> ancestorObjects = new HashMap<>(); 6 7 private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, 8 String parentPrefix, CacheKey parentRowKey, boolean newObject) { 9 boolean foundValues = false; 10 for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) { 11 final String nestedResultMapId = resultMapping.getNestedResultMapId(); 12 if (nestedResultMapId != null && resultMapping.getResultSet() == null) { 13 try { 14 final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping); 15 final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix); 16 if (resultMapping.getColumnPrefix() == null) { 17 // 为未声明列前缀的 result_mapping 封装循环引用对象 18 Object ancestorObject = ancestorObjects.get(nestedResultMapId); 19 if (ancestorObject != null) { 20 if (newObject) { 21 linkObjects(metaObject, resultMapping, ancestorObject); 22 } 23 continue; 24 } 25 } 26 // 同样创建缓存 KEY,并从循环应用缓存中获取已经创建但可能未完成一对多和多对一关系的对象 27 final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix); 28 final CacheKey combinedKey = combineKeys(rowKey, parentRowKey); 29 Object rowValue = nestedResultObjects.get(combinedKey); 30 boolean knownValue = rowValue != null; 31 instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); 32 if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) { 33 // 获取该行数据 34 rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue); 35 if (rowValue != null && !knownValue) { 36 // 封装到结果对象中 37 linkObjects(metaObject, resultMapping, rowValue); 38 foundValues = true; 39 } 40 } 41 } catch (SQLException e) { 42 throw new ExecutorException( 43 "Error getting nested result map values for '" + resultMapping.getProperty() + "'. Cause: " + e, e); 44 } 45 } 46 } 47 return foundValues; 48 } 49 50 private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) { 51 final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); 52 // 如果是一对多关系,则添加到对应集合中 53 if (collectionProperty != null) { 54 final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty); 55 targetMetaObject.add(rowValue); 56 } else { 57 // 否则直接为对应字段赋值 58 metaObject.setValue(resultMapping.getProperty(), rowValue); 59 } 60 } 61}
值得关注的是该方法中也调用了 getRowValue 方法,这样便形成了 递归调用,这也是解决循环引用问题的关键。另一个需要关注的是其中的 linkObjects 封装结果的方法,如果是一对多关系,它会向集合中进行添加,否则便直接为对象赋值。
ResultMap 的循环引用并不复杂,在本节中我们并没有深入源码的细节,更多关注的是解决循环引用的方法,即 递归 + 缓存 的解决方案,建议大家执行对应单测来熟悉流程并了解相关细节。
