解析resultMap元素的过程
在前面几篇文章中,我们详细的了解了resultMap
元素的属性和子元素定义,过程比较复杂,但总算还是有些收获.
- Mybatis源码之美:3.5.1.解析result元素
- Mybatis源码之美:3.5.2.负责一对一映射的association元素和负责一对多映射的collection元素
- Mybatis源码之美:3.5.3.负责动态处理数据的鉴别器--discriminator元素
- Mybatis源码之美:3.5.4.唯一标标识符--id元素
- Mybatis源码之美:3.5.5.配置构造方法的constructor元素
我也是写到resultMap
元素的时候,才忽然想明白一个道理:
只有实实在在的了解每个属性和元素的作用,我们才能更好的去理解
mybatis
解析resultMap
时所做的一些判断和处理.
所以,虽然介绍resultMap
元素的用法着实费了很大的功夫,但我觉得还是值得的.
辛苦了那么久,今天来点不一样的,换一个方式看看能不能更有意思的去学习源码,
那么,让我们开始吧~
闪亮登场环节
如果我们将mybatis
比作一个创业公司,resultMap
元素就好比是mybatis
的一个项目部,那么resultMap
元素的各个子元素就是形形色色的员工,角色不同,职责也不同.
resultMap
项目负责对接外部数据,并完成将外部数据转换为java
对象的业务.
在mybatis
公司初创时,针对resultMap
项目部,招进来许许多多的result
元素,刚开始时间短,看不出来什么不同,大家都负责最基础的普通属性转换工作.
但是随着公司业务的扩张,慢慢地,部分优秀的result
元素开始展露头角,因为在一些问题上总是能提出一些具有建设性的意见,于是得到了老板的赏识,升级成了id
元素.
虽然id
元素干的还是和result
元素一样的活,但是,多多少少身份是不一样了,至少在开会时,id
元素开始拥有了决策投票权,甚至所有的id
元素在一起就能拍板决定resultMap
项目部的大小事宜.
除此之外呢,还有一些result
元素深耕自己的岗位,日积月累的,在某一方面取得了不菲的成就,成为了能够独当一面的大牛,比如collection
和association
元素,他们分别成为了处理复杂对象和处理集合对象的业务领域专家,被公司委以重任,根据公司需要,他们随时可以拉起团队,成立一个和resultMap
职能一样的项目部.
公司内还有一个比较特殊的discriminator
元素,他是公司老板的亲大爷,见多识广,位高权重,现担任传达室大爷一职,你别看大爷上年纪了,但是本事可不小,一些比较复杂多变的问题基本都得靠discriminator
大爷来解决,当然大爷不会真动手干活,基本就是动动嘴皮子,分析分析业务,把业务合情合理的分配给整天屁颠屁颠的跟在他屁股后面的case
元素来处理.
case
元素也是从result
元素的位置上一点点摸爬滚打走上来的,他们虽然是discriminator
元素的儿子,但你可不要一厢情愿的以为case
元素是不学无术的富二代,这些case
元素也是个顶个儿的人才,个个都能独当一面.
甚至在resultMap
内部,大家将collection
,association
以及case
三元素称为嵌套映射的三巨头,人送外号小resultMap
.
除此之外呢,传说在resultMap
内部还有一个神龙见首不见尾的技术总监,名为constructor
,constructor
元素可不简单,他控制着resultMap
项目部工作成果的交付,公司可以没有他的身影,但是却一定流传着他的大名,他一现身,如何将数据转换为java
对象这件事就得按照他的想法来了.
在constructor
手下有一个单独的部门用来完成java
对象的构建工作,在这个部门完成java
对象的构建操作之前,其他元素啥事都不能干,都得等着他们.
这个部门里待的元素虽然也是从result
这个职位上走过来的,但是地位却有所不同.
公司的元老级元素arg
就是其中之一,公司初创的时候他就在了,一步步的从result
元素走到这个位置,虽然本事没啥长进,职位也没往上升,但多多少少算是个老人了,大家或多或少的也给他点面子.
但是另一个元素,idArg
就不一样了,他是arg
元素中的精英,精英中的精英,人称小技术总监,他可是和id
元素一样,具有公司的决策投票权的.
所以你别看idArg
和id
两个元素表面上只能干最普通的活,但是人家可以参与项目部决策,拿项目部分红的,甚至在一些人眼里,他俩能代表整个resultMap
项目部.
物以类聚,元素以群分
虽然resultMap
项目部不大,在mybaits
公司内部,干的也是数据清洗的脏话累活,手底下干活的元素也不多,但是他的的确确是mybaits
公司的核心项目部.
通常来说,你要是有数据清洗的项目,你就找mybaits
公司的前台填个表,上面详细的列上你需要哪些人,干哪些事.
你要是不知道这个单子应该怎么填,你就看看前面的几篇文章,或者看看resultMap
项目部的员工介绍:
比如id
,result
这俩哥们,他俩就非常擅长把数据转换为简单的属性值,一般情况下,简单的属性转换操作,找这哥俩准没错.
要是有些数据的构建比较复杂,那就得找技术总监constructor
元素处理了,constructor
人狠话不多,做事干净利索,拿个小本本稍微记一下,就把数据下放到idArg
和arg
手里去了.
前面咱也说了idArg
,arg
,id
,result
这四个哥们干的都是些简单的数据处理操作.你要是非得拿复杂数据来找他们处理,idArg
和arg
俩哥们倒是也能解决,他俩直接把数据转包给其他resultMap
项目部,让转包公司处理就行了.
但是id
和result
这俩哥们就不一样了,他俩要是能搭理你一下都算我输.
所以说,你要是真有些数据要转换为复杂对象,还是得找collection
和association
元素,这俩哥们全能,简单的活他俩能干,如果是复杂的活,他俩转包也好,自己拉团队也好,都能给你整的明明白白的.
要是有些数据比较复杂,涉及到的场景是会发生变化的,那这时候你就得找年纪大,经验足,察言观色能力的强的discriminator
大爷了,大爷手一背,墨镜一戴,就为涉及到的每一种场景,都分配了一个case
元素.
跟屁虫case
元素接到数据之后,转包也好,自己拉团队也好,处理起来那也是一点也不含糊的,毫不逊色于collection
和association
元素.
你看,这就是resultMap
公司,能把数据给你安排的明明白白.
现在你知道单子怎么填了吧,你把单子填上之后,mybatis
就开始根据你的单子给你立项,成立一个单独的resultMap
项目部来完成你的需求.
解析resultMap元素的基本流程
要知道,在一个resultMap
项目里面,真正干活的元素也就那么几个,像discriminator
和constructor
这种元素,他们属于领导型,主要是负责分配活.
真正干活的是那些从result
位置上一步一步摸爬滚打上来的元素:id
,result
,idArg
,arg
,association
,collection
,以及case
.
比如id
,result
,idArg
,arg
这四个元素就是脚踏实地,老老实实的负责简单数据转换的工作,虽然职位名称不一样吧,但是干的活基本一致.
剩下的嵌套映射三巨头的工作和上面四个又有点不一样,因为三巨头
是能带团队的,所以他们自己可以根据工作需要单独成立一个子resultMap
项目部,或者找个其他的resultMap
项目部来帮他们完成工作.
下面是resultMap
的所有子元素定义:
我们将上面的思维导图去重之后重新整理:
娱乐时间结束,言归正传.
我们可以大致将resultMap
的子元素分为两类:一类是不负责实际属性转换操作的标志性的元素,一类是实际完成属性转换的元素.
标志性元素discriminator
和constructor
因为定义和作用完全不同,所以在解析时需要单独处理,剩余的其他元素则可以放在一起处理.
从这个角度上出发,我们可以将resultMap
的子元素分为三部分来解析,分别是constuctor
元素,discriminator
元素以及剩余的其他元素.
实际上,mybatis
也是这样解析的.
resultMap元素的解析入口
XMLMapperBuilder
的configurationElement()
方法是resultMap
元素的解析入口:
private void configurationElement(XNode context) {
try {
// 省略...
// 解析并注册resultMap元素
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 省略...
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
configurationElement()
方法将从mapper
文件中获取到的所有resultMap
元素定义一股脑的交给了resultMapElements()
方法来完成解析操作:
private void resultMapElements(List<XNode> list) throws Exception {
for (XNode resultMapNode : list) {
try {
// 解析ResultMap节点
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
// 在内部实现中,未完成解析的节点将会被放至Configuration#incompleteResultMaps中
}
}
}
resultMapElements()
方法则依次将每个resultMap
元素定义交给resultMapElement()
方法来完成resultMap
元素的解析注册操作:
/**
* 解析ResultMap节点
*
* @param resultMapNode ResultMap节点
*/
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
return resultMapElement(resultMapNode, Collections.<ResultMapping>emptyList(), null);
}
resultMapElement方法简介
不过,实际完成解析工作的方法是resultMapElement()
方法的另一个重载实现:
/**
* 解析ResultMap元素,关于ResultMap的各个子元素的作用可以参考文档{@link https://blog.csdn.net/u012702547/article/details/54599132}
* <p>
* 该方法并不是单纯的只用于解析ResultMap元素,而是用于解析具有ResultMap性质的元素,该方法的调用方,目前 有两个,一个是用来解析`ResultMap`元素,
* 另一个使用该方法来解析association/collection/discriminator的case元素。
*
* @param resultMapNode resultMapNode节点
* @param additionalResultMappings 现有的resultMapping结合
* @param enclosingType 返回类型
*/
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取唯一标志,有趣的是association/collection/discriminator的case元素都没有ID属性,所以该ID会根据嵌套的上下文来生成。
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
// 获取返回类型 依次读取:【type】>【ofType】>【resultType】>【javaType】
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 获取当前ResultMap是否继承了其他ResultMap
String extend = resultMapNode.getStringAttribute("extends");
// 获取自动映射标志
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// 解析出返回类型
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
// 嵌套映射时,外部对象属性类型定义优先级较低
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
// 返回结果定义
List<ResultMapping> resultMappings = new ArrayList<>();
// 添加所有额外的ResultMap集合
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
// 开始处理ResultMap中的每一个子节点
for (XNode resultChild : resultChildren) {
// 获取每一个ResultMap的子节点 处理constructor节点,该节点用来配置构造方法
if ("constructor".equals(resultChild.getName())) {
// 处理constructor节点
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
// 处理discriminator节点(鉴别器)
// 通过配置discriminator节点可以实现根据查询结果动态生成查询语句的功能
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
// 获取ID标签
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
// 添加ID标记
flags.add(ResultFlag.ID);
}
// 添加ResultMapping配置
resultMappings.add(
buildResultMappingFromContext(
resultChild
, typeClass
, flags
)
);
}
}
// 构建ResultMap解析器
ResultMapResolver resultMapResolver = new ResultMapResolver(
builderAssistant
, id /*resultMap的ID*/
, typeClass /*返回类型*/
, extend /*继承的ResultMap*/
, discriminator /*鉴别器*/
, resultMappings /*内部的ResultMapping集合*/
, autoMapping /*自动映射*/
);
try {
// 解析ResultMap
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
// 解析ResultMap发生异常,将奖盖ResultMap放入未完成解析的ResultMap集合.
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)
方法最终会返回一个ResultMap
对象,该方法的实现比较长,我们一点一点的看.
该方法返回的
ResultMap
对象维护了整个resultMap
元素中的所有配置,他的属性很多,功能也很强大,具体的作用我们会在后续的解析过程中给出。
我们先要了解的第一点是:resultMapElement
方法解析的resultMap
元素是指所有具有resultMap
元素性质的元素,因此resultMapElement
方法还被用来解析association
,collection
以及case
元素.
resultMapElement方法的入参介绍
resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)
方法有三个入参,分别是resultMapNode
,additionalResultMappings
以及enclosingType
.
resultMapNode
其中resultMapNode
比较好理解,他表示所有具有resultMap
元素性质的元素定义,注意,他不是单纯的只代表resultMap
元素,而是表示所有具有resultMap
性质的元素,
比如他还可以表示association
,collection
以及discriminator
的case
元素:
<!ELEMENT resultMap (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ELEMENT association (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ELEMENT collection (constructor?,id*,result*,association*,collection*, discriminator?)>
<!ELEMENT case (constructor?,id*,result*,association*,collection*, discriminator?)>
additionalResultMappings
additionalResultMappings
表示现有的ResultMapping
集合,该参数只有在解析discriminator
元素时才有数据,其他时候均为空集合.
更多关于additionalResultMappings
参数的介绍,我们放在解析discriminator
子元素的内容中来讲.
因为根据DTD
定义来看,为具有resultMap
性质的元素配置discriminator
子元素时,discriminator
子元素必须声明在元素的尾部:
这样我们在解析时,必须是先解析出其余的元素配置,才会解析discriminator
子元素.
enClosingType
enClosingType
表示当前正在解析的resultMap
所属的resultMap
对应的java
类型,该参数有可能为空.
假设我们现有如下resultMap
配置:
<resultMap id="userWithNotNullColumn" type="org.apache.learning.result_map.association.User" autoMapping="true">
<association property="role" column="role_id" resultMap="role" columnPrefix="role_" notNullColumn="name"/>
</resultMap>
在我们解析id
为userWithNotNullColumn
的resultMap
元素时,因为resultMap
不是嵌套的结果映射配置,他没有所属的resultMap
,所以在解析该元素是enclosingType
参数为null
.
但是当我们解析嵌套在resultMap
内部的association
元素时,因为该元素属于id
为userWithNotNullColumn
的resultMap
元素,所以enclosingType
参数的值是org.apache.learning.result_map.association.User
.
总结
additionalResultMappings
和enClosingType
这两个属性可能现在比较难理解,但是当我们深入到resultMap
元素的解析过程之后,我们就会很容易的理解这两个参数的含义.
resultMapElement方法的详解(解析resultMap元素)
了解了方法入参之后,我们回到resultMapElement()
方法的解析过程中来:
// 获取唯一标志,有趣的是association/collection/discriminator的case元素都没有ID属性,所以该ID会根据嵌套的上下文来生成。
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
在解析这种具有resultMap
性质的元素的时候,mybatis
首先会读取他的id
属性,这个id
属性的值将会作为生成该元素唯一标志的依据.
不过,有一点需要注意,除了resultMap
元素以外,association
,collection
以及discriminator
的case
元素都没有提供id
属性的定义,他们唯一标志的生成是根据元素定义在DOM
树中实际所处的位置来确定的,大致实现原理就是递归拼接元素类型和名称直到顶层元素为止,具体实现代码如下:
/**
* 基于元素的层级结构生成唯一标志,
* 大概就是这样:
* mapper_resultMap[test_resultMap]_collection[arrays]
*/
public String getValueBasedIdentifier() {
StringBuilder builder = new StringBuilder();
XNode current = this;
// 一直递归处理到顶级元素
while (current != null) {
if (current != this) {
builder.insert(0, "_");
}
// 按照优先级一次读取 id, value ,property属性
String value = current.getStringAttribute(
"id",
current.getStringAttribute(
"value",
current.getStringAttribute(
"property"
, null
)
)
);
// 将value值中所有的.都替换成下划线
// [value]
if (value != null) {
value = value.replace('.', '_');
builder.insert(0, "]");
builder.insert(0,
value);
builder.insert(0, "[");
}
// 元素名称[value]
builder.insert(0, current.getName());
current = current.getParent();
}
return builder.toString();
}
在完成了ResultMap
对象的唯一标志的生成工作之后,Mybatis
接着会获取该元素所对应的java
类型,因为不同类型的元素对于java
类型定义的属性名称也有些许的不同之处,因此Mybatis
在获取java
类型的时候会按照顺序依次读取type
,ofType
,resultType
,javaType
属性:
// 获取返回类型 优先级:【type】>【ofType】>【resultType】>【javaType】
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
在获取到该元素的java
类型之后,Mybatis
会通过读取该元素定义的extends
属性,获取当前元素所继承的resultMap
元素的定义。
之后再通过autoMapping
属性来获取当前resultMap
的自动映射行为:
// 获取当前ResultMap是否继承了其他ResultMap
String extend = resultMapNode.getStringAttribute("extends");
// 获取自动映射标志
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
然后解析类型别名,尝试获取当前resultMap
对应的java
类型:
// 解析出返回类型
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
// 嵌套映射时,外部对象属性类型定义优先级较低
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
如果当前resultMap
没有直接指定对应的java
类型,mybatis
会尝试通过上下文来推断出合适的类型,负责推断类型的方法是inheritEnclosingType()
方法:
protected Class<?> inheritEnclosingType(XNode resultMapNode, Class<?> enclosingType) {
// 一对一集合映射
if ("association".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) {
// 通过属性定义推断java类型
String property = resultMapNode.getStringAttribute("property");
if (property != null && enclosingType != null) {
MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
return metaResultType.getSetterType(property);
}
} else if ("case".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) {
// 鉴别器
return enclosingType;
}
return null;
}
inheritEnclosingType()
方法的实现并不复杂,首先他不会处理配置了resultMap
属性的元素,因为无论是association
元素也好还是case
元素也好,如果他们配置了resultMap
属性,那就意味着该元素对应属性的类型转换处理操作是由被引用的resultMap
来处理的,当前resultMap
无需处理,因此,当前方法也就无需进行类型推断操作.
针对association
元素,因为在设计上association
元素的作用就是为某一对象的属性配置一个复杂对象的映射,因此我们可以借助于属性名称和属性所属对象的类型通过反射获取到association
元素所对应的类型定义.
而针对case
元素,其父元素discriminator
的javaType
属性是必填的,这个属性直接就表明了当前鉴别器所对应的java
类型,discriminator
的javaType
属性定义在解析时是作为enclosingType
参数传递进来,因此从理论上来讲直接返回enclosingType
参数即可.
<!ATTLIST discriminator
column CDATA #IMPLIED
javaType CDATA #REQUIRED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
那么同为嵌套映射三巨头的collection
为什么没有进行类型推断操作呢?
这是因为collection
元素用于配置集合映射,所以解析collection
时,enclosingType
参数对应的是一个集合类型,根据mybatis
现有配置,我们无法为集合指定泛型,因此除非用户明确指出,否则我们无法通过反射或者其他方式获取集合中元素的类型.
在得到当前resultMap
元素对应的java
类型之后,mybatis
创建了一个用于存放ResultMapping
对象的resultMappings
集合:
// 返回结果定义
List<ResultMapping> resultMappings = new ArrayList<>();
// 添加所有额外的ResultMap集合
resultMappings.addAll(additionalResultMappings);
之后会依次将当前resultMap
的所有子元素全部转换为ResultMapping
对象,并保存到resultMappings
集合中.
探究resultMap子元素的解析操作
将resultMap
的子元素转换为ResultMapping
对象的操作,根据子元素的类型和作用,基本可以分为三类:构造参数配置,鉴别器配置,以及标准属性映射配置.
List<XNode> resultChildren = resultMapNode.getChildren();
// 开始处理ResultMap中的每一个子节点
for (XNode resultChild : resultChildren) {
// 获取每一个ResultMap的子节点 处理constructor节点,该节点用来配置构造方法
if ("constructor".equals(resultChild.getName())) {
// 处理constructor节点
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
// 处理discriminator节点(鉴别器)
// 通过配置discriminator节点可以实现根据查询结果动态生成查询语句的功能
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
// 获取ID标签
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
// 添加ID标记
flags.add(ResultFlag.ID);
}
// 添加ResultMapping配置
resultMappings.add(
buildResultMappingFromContext(
resultChild
, typeClass
, flags
)
);
}
}
这三类的解析操作则分别对应着上面代码中的三条分支语句.
解析标准属性映射配置
其中最基本的的操作是标准属性映射配置,它对应的代码块是上面代码中的最后一个else
语句:
// 获取ID标签
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
// 添加ID标记
flags.add(ResultFlag.ID);
}
// 添加ResultMapping配置
resultMappings.add(
buildResultMappingFromContext(
resultChild
, typeClass
, flags
)
);
这一部分代码主要用来处理id
,result
,association
以及collection
四个元素.
方法实现比较简单,除了会针对id
元素的配置单独添加一个ResultFlag.ID
标记之外,剩下的操作都交给了buildResultMappingFromContext()
方法来完成.
ResultFlag
是一个枚举对象,他有两个实现:ID
和CONSTRUCTOR
,分别用来为当前配置的元素添加ID
和构造参数标识.
public enum ResultFlag {
ID, CONSTRUCTOR
}
解析构造参数配置
构造参数配置的处理逻辑和标准属性映射配置的处理逻辑非常相似,它对应着第一个if
语句分支:
// 获取每一个ResultMap的子节点 处理constructor节点,该节点用来配置构造方法
if ("constructor".equals(resultChild.getName())) {
// 处理constructor节点
processConstructorElement(resultChild, typeClass, resultMappings);
}
在processConstructorElement()
方法中,mybatis
读取出constructor
元素的所有arg
和idArg
子元素定义.
因为这两个元素用于配置构造参数,所以需要为他们添加上ResultFlag.CONSTRUCTOR
标记,针对idArg
还要额外增加ResultFlag.ID
标记.
添加完标记之后,剩下的操作就和标准属性映射配置的处理一样了,殊途同归,将剩余的操作都交给了buildResultMappingFromContext()
方法来完成:
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
// 获取constructor所有的子节点
List<XNode> argChildren = resultChild.getChildren();
for (XNode argChild : argChildren) {
List<ResultFlag> flags = new ArrayList<>();
// 表示属于构造参数
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
// 主键标记
flags.add(ResultFlag.ID);
}
resultMappings.add(
buildResultMappingFromContext(
argChild /*constructor元素的idArg或者arg子元素*/
, resultType/*构造方法对应的java对象*/
, flags /*参数类型标记*/
)
);
}
}
因此buildResultMappingFromContext()
方法实际处理的是id
,result
,association
,collection
,arg
以及idArg
六个子元素.
简单理解buildResultMappingFromContext方法的入参
buildResultMappingFromContext
方法是用来构建ResultMapping
对象实例的,他有三个参数content
,resultType
以及flags
.
其中content
是一个XNode
对象实例,他表示一个resultMap
元素的子元素,他可以是arg
,idArg
,collection
,association
,result
以及id
.
resultType
参数则表示这个元素对应的java
类型。
flags
表示这个元素的性质,比如这个元素是不是一个构造参数,或者这个元素是不是一个数据库主键。
buildResultMappingFromContext方法的解析操作
buildResultMappingFromContext()
方法的实现谈不上复杂与否,基本就是简单逻辑的堆砌,我们先总览一下代码实现:
/**
* 构建resultMapping对象
*
* @param context ResultMapping代码块
* @param resultType 返回类型
* @param flags 参数类型标记(构造?主键?)
*/
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
// 如果当前节点定义的是一个构造参数,那么读取的是其name属性(形参名称)。
property = context.getStringAttribute("name");
} else {
// 不是构造参数,读取property属性
property = context.getStringAttribute("property");
}
// 对应的JDBC列名称
String column = context.getStringAttribute("column");
// 对应的java类型
String javaType = context.getStringAttribute("javaType");
// 对应的jdbc类型
String jdbcType = context.getStringAttribute("jdbcType");
// 是否引用了其他select语句
String nestedSelect = context.getStringAttribute("select");
/*
resultMap中可以包含association或者collection这种复合节点,这些复合类型可以使用外部定义的resultMap或者内嵌的resultMap,
因此针对这里的处理逻辑是:如果有resultMap就获取,没有则动态生成一个,动态生成的resultMap的唯一标志是基于XNode#getValueBasedIdentifier计算得来的。
*/
String nestedResultMap = context.getStringAttribute(
"resultMap",/*使用指定的resultMap*/
processNestedResultMappings(context, Collections.<ResultMapping>emptyList(), resultType)/*这里表示默认值,如果没有则动态生成一个ResultMap*/
);
// 默认情况下,子对象仅在至少一个列映射到其属性非空时才创建。
// 通过对这个属性指定非空的列将改变默认行为,这样做之后Mybatis将仅在这些列非空时才创建一个子对象。
// 可以指定多个列名,使用逗号分隔。默认值:未设置(unset)。
String notNullColumn = context.getStringAttribute("notNullColumn");
// 当连接多表时,你将不得不使用列别名来避免ResultSet中的重复列名。
// 因此你可以指定columnPrefix映射列名到一个外部的结果集中。
String columnPrefix = context.getStringAttribute("columnPrefix");
// 类型转换处理器
String typeHandler = context.getStringAttribute("typeHandler");
// 获取resultSet集合
String resultSet = context.getStringAttribute("resultSet");
// 标识出包含foreign keys的列的名称
String foreignColumn = context.getStringAttribute("foreignColumn");
// 懒加载
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
// 解析java类型
Class<?> javaTypeClass = resolveClass(javaType);
// 解析类型处理器
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
// 解析出jdbc类型
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
// 创建最终的resultMap
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
buildResultMappingFromContext()
方法首先会根据元素的不同使用不同的方法来获取元素对应的属性名称.
因为idArg
和arg
两个元素特殊的DTD
定义,所以在获取这两个元素的属性名称时,不能通过property
属性,而是要通过name
属性:
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
// 如果当前节点定义的是一个构造参数,那么读取的是其name属性(形参名称)。
property = context.getStringAttribute("name");
} else {
// 不是构造参数,读取property属性
property = context.getStringAttribute("property");
}
前面的文章中提到过,
arg
和idArg
元素的name
属性,对应的是java
构造方法的形参名称,基于形参名称来配置构造参数,我们就可以忽略掉具体的构造参数的顺序了。
// 对应的JDBC列名称
String column = context.getStringAttribute("column");
// 对应的java类型
String javaType = context.getStringAttribute("javaType");
// 对应的jdbc类型
String jdbcType = context.getStringAttribute("jdbcType");
获取java
字段名称之后,buildResultMappingFromContext()
方法会进行一些基础属性的获取工作,比如获取对应的java
类型,对应的数据库列名称,对应的数据库类型等等.
// 是否引用了其他select语句
String nestedSelect = context.getStringAttribute("select");
之后,会判断当前元素有没有配置select
属性.
idArg
,arg
,association
,collection
这四个元素都可以配置select
属性,select
属性可以引用一个现有的映射声明语句。
处理完select
属性之后,开始处理resultMap
属性的配置.
/*
resultMap中可以包含association或者collection这种复合节点,这些复合类型可以使用外部定义的resultMap或者内嵌的resultMap,
因此针对这里的处理逻辑是:如果有resultMap就获取,没有则动态生成一个,动态生成的resultMap的唯一标志是基于XNode#getValueBasedIdentifier计算得来的。
*/
String nestedResultMap = context.getStringAttribute(
"resultMap",/*使用指定的resultMap*/
processNestedResultMappings(context, Collections.<ResultMapping>emptyList(), resultType)/*这里表示默认值,如果没有则动态生成一个ResultMap*/
);
虽然arg
,idArg
,collection
以及association
这四个元素都能够配置resultMap
属性,但是arg
和idArg
只能引用现有的结果映射配置,而collection
,association
这两个元素还能直接用来配置嵌套结果映射.
因此针对collection
和association
这两个元素,还会调用processNestedResultMappings()
方法解析嵌套的结果映射配置.
processNestedResultMappings()
方法是用来解析嵌套映射三巨头的,因此除了collection
和association
元素之外,case
元素也能被该方法处理,他负责将嵌套结果映射配置解析成相应的ResultMap
对象,并返回ResultMap
对象的全局引用ID
.
/**
* 处理嵌套的ResultMap,作用于处理association或者collection节点、
* <p>
* resultMap中可以包含association或者collection这种复合节点,这些复合类型可以使用外部定义的resultMap或者内嵌的resultMap,
* 因此针对这里的处理逻辑是:如果有resultMap就获取,
* 没有则动态生成一个,动态生成的resultMap的唯一标志是基于XNode#getValueBasedIdentifier计算得来的。
*
* @param context 父级XML代码块
* @param resultMappings 已有的resultMapping集合
* @param enclosingType 返回类型
*/
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) throws Exception {
if ("association".equals(context.getName())
|| "collection".equals(context.getName())
|| "case".equals(context.getName())) {
// association和collection property有select属性,这里只处理非select参数的代码块
/*
* select可以指定另外一个映射语句的ID,加载这个属性映射需要的复杂类型。
* 在列属性中指定的列值将会被传递给目标select语句中作为参数。
*/
if (context.getStringAttribute("select") == null) {
// 没有指定select属性
validateCollection(context, enclosingType);
// 解析嵌套的resultMap元素
ResultMap resultMap =
resultMapElement(context, resultMappings, enclosingType);
return resultMap.getId();
}
}
return null;
}
processNestedResultMappings()
方法不会处理指定了select
属性的元素,这是因为同一条属性映射配置不能在指定select
属性的同时配置嵌套映射.
processNestedResultMappings()
方法的实现并不复杂,在单独对collection
元素做了校验之后,就把创建嵌套结果映射的任务交给resultMapElement()
方法完成,这时候,我们可以看到processNestedResultMappings()
方法和resultMapElement()
方法二者之间构成了递归调用的关系:
@startuml
autonumber
participant "resultMapElement()" as rme
participant "buildResultMappingFromContext()" as brmfc
participant "processNestedResultMappings()" as pnrm
activate rme
opt 处理标准属性映射配置
rme -> brmfc ++ : 处理元素定义
opt 未引用其他结果映射
brmfc -> pnrm ++ : 处理嵌套结果映射
opt 未配置select属性
== 开始递归调用 ==
pnrm -[#red]> rme ++ #red: 解析处理嵌套结果映射 <color:red> <b> [递归调用]
return
== 结束递归调用 ==
end
return
end
return
end
@enduml
processNestedResultMappings()
方法为什么需要单独校验collection
元素呢?
我们需要先明确一点,调用processNestedResultMappings()
方法的前提是子元素没有配置resultMap
属性,在这个前提下,association
元素和case
元素的类型早就在前面的处理过程中加载或者推断出来了.
因此,此时只有collection
元素才有可能无法获取对应的集合类型.所以在真正解析collection
元素之前,我们需要校验collection
元素是否定义了对应的java
类型.
validateCollection()
方法实现比较简单,因为如果collection
元素配置了resultMap
或者resultType
属性,mybatis
是可以根据这两个属性间接得到集合类型的,因此validateCollection()
方法主要是校验在没有配置这些属性的时候,能否通过反射来获取集合类型:
/**
* 验证集合
*
* @param context XML代码块
* @param enclosingType 返回类型
*/
protected void validateCollection(XNode context, Class<?> enclosingType) {
if ("collection".equals(context.getName()) /*处理Collection集合*/
&& context.getStringAttribute("resultMap") == null/*没有定义resultMap*/
&& context.getStringAttribute("resultType") == null/*没有定义resultType*/
) {
// 解析collection内部块
// 获取将要返回类型的类型元数据
MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
/*获取其property值,该值对应返回类的字段*/
String property = context.getStringAttribute("property");
if (!metaResultType.hasSetter(property)) {
throw new BuilderException(
"Ambiguous collection type for property '" + property + "'. You must specify 'resultType' or 'resultMap'.");
}
}
}