作者:京东保险 王奕龙
本节我们来了解 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 在处理循环引用时,会根据引用关系创建最外层对象,每遇到新的引用,都会创建新的对象,并将这些对象“存”起来,当遇到现有对象需要被引用时,则会从“缓存”中取,不断地回归处理引用关系,这和算法中“递归”的思想一致,接下来我们看一下源码中是如何处理的,我们直接看 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
对象,并向其中表示一对多关系的集合中添加值。以我们的样例数据为例,查询完毕后的对象如下所示:
可以发现 resource1
的 principals
字段会对应多个 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 的循环引用并不复杂,在本节中我们并没有深入源码的细节,更多关注的是解决循环引用的方法,即 递归 + 缓存 的解决方案,建议大家执行对应单测来熟悉流程并了解相关细节。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。