需求描述

修改已有的结果集,可以对查询的字段与 Java 属性直接进行任意的映射。

实际项目中,为了防止修改表结构而造成锁表,因此采用拓展表放置新的字段,为了不修改原有业务逻辑,故需要使用切面来处理结果集新增的部分字段,将其映射到 Map 中。

解决方案

ps: 通过修改 XML 可以完成所有复杂查询,但要考虑到方案适配的通用性,XML ヾ(•ω•`)o
Mybatis 内置的 ResultHandler 拦截器可以对其 ResultMap 对象进行拦截。

关键的数据结构

ResultMap 是 Mybatis 的结果集映射对象,与 XML 中 相对应,其中通过 ResultMapping 定义了每一列字段与 Java 属性之间的映射关系,具体数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class ResultMap {
private Configuration configuration;

// 唯一标识符
private String id;
// 映射类型,Map 或 Java Bean
private Class<?> type;
// 每一列对应的映射关系
private List<ResultMapping> resultMappings;
// 主键的映射关系,有助于生成缓存提升性能,可为空
private List<ResultMapping> idResultMappings;
// 构造器映射关系
private List<ResultMapping> constructorResultMappings;
// 属性映射关系
private List<ResultMapping> propertyResultMappings;
// 被映射的列
private Set<String> mappedColumns;
// 被映射的属性
private Set<String> mappedProperties;
// 鉴别器,在不同字典值分别使用不同的映射关系时使用
private Discriminator discriminator;
// 是否存在嵌套的结果集映射,一对多、一对一时为 true
private boolean hasNestedResultMaps;
// 是否存在嵌套查询,与 <select/> 标签相对于,存在 N+1
private boolean hasNestedQueries;
// 是否自动映射,列名与属性名完全一致时启用可以取消显示映射
private Boolean autoMapping;
}


private Configuration configuration;
// 属性名,Map 的 key 值,或 Java Bean 的属性,自动映射时会按照全局策略转换字符串
// 默认时下划线转小驼峰
private String property;
// 列名,该列是嵌套结果集时可以为空,否则需要与属性值一一对应
private String column;
// Java 类型
private Class<?> javaType;
// Jdbc 类型
private JdbcType jdbcType;
// 类型处理器,常用的基本类型可以为空,走内置处理器,对于一些特殊的,如 list(程序) 转 string(数据库),
// 则需要指定自定义的类型处理器,继承 TypeHandler 实现方法即可
private TypeHandler<?> typeHandler;
// 嵌套结果集的 Id
private String nestedResultMapId;
// 嵌套查询的 Id
private String nestedQueryId;
// 非空列
private Set<String> notNullColumns;
// 列前缀,自动拼接在列名前,可以为空
private String columnPrefix;
// 主键、构造器标识
private List<ResultFlag> flags;
private List<ResultMapping> composites;
// 结果集
private String resultSet;
// 外键列
private String foreignColumn;
// 是否启用懒加载,嵌套查询时生效
private boolean lazy;
}

获取 ResultMap 对象

ResultMap 可以通过 MappedStatement 获取,且获取的是一个不可修改的 List 集合。因此 ResultMap 的修改要点:

  1. 遍历所有结果集映射;
  2. 反射重置结果集列表;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ResultHanlderInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) {
// 获取默认处理器
DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) invocation.getTarget();
// mybatis 元数据对象,辅助工具,屏蔽反射受检异常
MetaObject metaStatementHandler = SystemMetaObject.forObject(defaultResultSetHandler);
// 获取映射语句,当前执行 SQL 的所有信息
// 其中存在 List<ResultMap> 属性,每一个 ResultMap 对应一个 Java Bean,
// 一般只有一个值,即 SQL 的返回值
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("mappedStatement");
// 处理 ResultMap
for (ResultMap resultMap: mappedStatement.getResultMap) {
// 重建 ResultMap
}
}
}

重建 ResultMap

ResultMap 的获取并不复杂,事实上很轻易的就可以拿到,通过调试或者查看源码,可知,通过 getter 方法可以得到列映射关系,ResultMapping,同样这也是不可修改的 List 集合,但不同的是,直接通过反射修改是不会生效的,所有的(列或结果集)映射关系必须要先向 mybatis 的 configuration 注册才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 重建 ResultMap
public static ResultMap rebuild(MappedStatement ms, ResultMap resultMap) {
MapperBuilderAssistant builderAssistant = getMapperBuilderAssistant(ms);
// 先获取原有的 ResultMapping 列表,仅对要修改的部分做处理(增、改、删)
List<ResultMapping> resultMappings = Lists.newArrayList(resultMap.getResultMappings().iterator());
// 增加一个新的映射
resultMappings.add(getResultMapping());
// 注册 ResultMap 到 configuration
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant,
"id,唯一标识,不可重复"),
resultMap.getType(), null, null,
// 将 resultMappings 设为不可变 list
Collections.unmodifiableList(resultMappings), 是否自动映射);
return resultMapResolver.resolve();
}

// 构建一个新的列映射关系
public ResultMapping getResultMapping() {
return new ResultMapping.Builder(builderAssistant.getConfiguration(),
getProperty("属性名"))
.column("列名"))
.typeHandler(类型处理器.class)
.jdbcType(JdbcType.value))
.javaType(Java 类型.class)
.nestedResultMapId("嵌套的 ResultMap id, 需要先注册")
.build())
}

/**
* 获取映射器构建助手
*
* @param ms 映射语句
* @return 映射器构建助手
*/
private static MapperBuilderAssistant getMapperBuilderAssistant(MappedStatement ms) {
Configuration configuration = ms.getConfiguration();
String resource = ms.getResource();
String nameSpace = ms.getId().substring(0, ms.getId().lastIndexOf("."));
MapperBuilderAssistant builderAssistant = new MapperBuilderAssistant(configuration, resource);
builderAssistant.setCurrentNamespace(nameSpace);
return builderAssistant;
}

注意事项

  1. 对应 (XML 文件)没有 ResultMap(或使用 ResultType)的结果集,需要设置自动映射(autoMapping = true),否则可能出现属性丢失的问题;
  2. 仅仅(通过反射)修改 ResultMap 的 resultMappings 属性不会生效,因为 ResultMapping 用于记录映射关系,却不是 Mybatis Configuration 真实的读取的信息,需要通过 ResultMapResolver 解析器读取信息,并注册到配置中心;
  3. 对于使用线程变量做拦截器标记时(如 PageHelper),在最终返回时拦截器结束时需要对线程变量进行 clear,谨防 OOM;
  4. 也可以在拦截器中直接读取 ResultSet,手动进行映射,或使用 Json 等工具,最终返回格式化后的对象,但需要考虑到列名与属性名不一样的场景,且对 Mybatis 映射产生破坏(不推荐);

总结

通过修改 ResultMap 可以任意的进行结果集映射,就像在 XML 中定义了 标签一样,但不同的是,其可以通过注解或参数等方式,对某个查询进行标记,可以在运行时动态的修改返回查询结果,且 ResultMap 会被 Mybaits 自动缓存。

评论