MyBatis 源码学习13——ResultMap
一、ResultMap:
ResultMap:
保存Java实体属性与数据库表字段之间的映射关系,是实现级联映射和懒加载机制的基础。
MyBatis是一个半自动化的ORM框架,可以将数据库中的记录转换为Java实体对象,但是Java实体属性通常采用驼峰命名法,而数据库字段习惯采用下画线分割命名法,因此需要用户指定Java实体属性与数据库表字段之间的映射关系。
MyBatis的Mapper配置中提供了一个<resultMap>
标签,用于建立数据库字段与Java实体属性之间的映射关系。
<resultMap>
子标签:
• <constructor>
:该标签用于建立构造器映射。该标签有两个子标签,<idArg>
标签用于配置主键映射,标记出主键,可以提高整体性能;<arg>
标签用于配置普通字段的映射。
• <id>
:用于配置数据库主键映射,标记出数据库主键,有助于提高整体性能。
• <result>
:用于配置数据库字段与Java实体属性之间的映射关系。
• <association>
:用于配置一对一关联映射,可以关联一个外部的查询Mapper或者配置一个嵌套的ResultMap。
• <collection>
:用于配置一对多关联映射,可以关联一个外部的查询Mapper或者配置一个嵌套的ResultMap。
• <discriminator>
:用于配置根据字段值使用不同的ResultMap。该标签有一个子标签,<case>
标签用于枚举字段值对应的ResultMap,类似于Java中的switch语法。
ResultMap解析过程:
MyBatis在启动时,所有配置信息都会被转换为Java对象,标签信息会转为ResultMap对象。
ResultMap类:
• Id:通过<resultMap>
标签的id属性和Mapper命名空间组成的全局唯一的Id。
• Type:通过<resultMap>
标签的type属性指定与数据库表建立映射的Java实体。
• resultMappings:通过<result>
标签配置的所有数据库字段与Java实体属性之间的映射信息。
• idResultMappings:通过<id>
标签配置的数据库主键与Java实体属性的映射信息。需要注意的是,<id>
标签与<result>
标签没有本质的区别。
• constructorResultMappings:通过<constructor>
标签配置的构造器映射信息。
• propertyResultMappings:通过<result>
标签配置的数据库字段与Java实体属性的映射信息。
• mappedColumns:该属性存放所有映射的数据库字段。当使用columnPrefix属性配置了前缀时,MyBatis会对mappedColumns属性进行遍历,为所有数据库字段追加columnPrefix属性配置的前缀。
• mappedProperties:该属性存放所有映射的Java实体属性信息。
• discriminator:该属性为在<resultMap>
标签中通过<discriminator>
标签配置的鉴别器信息。
• hasNestedResultMaps:该属性用于标识是否有嵌套的ResultMap,当使用<association>
或<collection>
标签以JOIN查询方式配置一对一或一对多级联映射时,<association>
或<collection>
标签相当于一个嵌套的ResultMap,因此hasNestedResultMaps属性值为true。
• hasNestedQueries:该属性用于标识是否有嵌套的查询,当使用<association>
或<collection>
标签关联一个外部的查询Mapper建立一对一或一对多级联映射时,hasNestedQueries属性值为true。
• autoMapping:autoMapping属性为true,表示开启自动映射,即使未使用<result>或<id>
标签配置映射字段,MyBatis也会自动对这些字段进行映射。
<resultMap>
标签解析生成ResultMap对象的过程:
MyBatis中的Mapper配置信息解析是通过XMLMapperBuilder类完成的,该类提供了一个parse()方法,用于解析Mapper中的所有配置信息:
在XMLMapperBuilder的parse()方法中,调用XMLMapperBuilder类的configurationElement()方法,然后**configurationElement()**方法又调用resultMapElements()方法对所有<resultMap>
标签进行解析。
resultMapElements()方法最终会调用重载的resultMapElement()方法对每个<resultMap>
标签进行解析:
1.首先获取<resultMap>
标签的所有属性信息,
2.然后对<id>
等子标签进行解析,通过buildResultMappingFromContext(),创建字段的ResultMapping对象
3.接着创建一个ResultMapResolver对象,
4.调用ResultMapResolver对象的resolve()方法返回一个ResultMap对象。
ResultMapResolver对象的resolve()方法调用了MapperBuilderAssistant的addResultMap()方法:
1.首先判断该ResultMap是否继承了其他ResultMap。如果是,则获取父ResultMap对象,然后去除父ResultMap中的构造器映射信息,将父ResultMap中配置的映射信息添加到当前ResultMap对象。
2.然后通过建造者模式在ResultMap.Builder类中创建一个ResultMap对象,然后为ResultMap对象的所有属性赋值。
3.把ResultMap对象添加到Configuration对象的属性resultMaps中,key为ResultMap对象的id,value为ResultMap对象。
二、映射实现原理:
StatementHandler组件与数据库完成交互后,会使用ResultSetHandler组件对结果集进行处理。
PreparedStatementHandler类的query()方法:
1.调用PreparedStatement对象的execute()方法完成与数据库交互,
2.调用ResultSetHandler对象的handleResultSets()方法对结果集进行处理。
ResultSetHandler接口只有一个默认的实现,即DefaultResultSetHandler类。
DefaultResultSetHandler类中handleResultSets()方法:
1.为了简化对JDBC中ResultSet对象的操作,将ResultSet对象包装成ResultSetWrapper对象,
2.然后获取MappedStatement对象对应的ResultMap对象,接着调用重载的handleResultSet()方法,handleResultSet()方法中做了一些逻辑判断,但最终都会调用DefaultResultSetHandler类的handleRowValues()方法。
在DefaultResultSetHandler类的handleRowValues()方法中处理结果集时,对嵌套的ResultMap和非嵌套ResultMap做了不同处理:
方法中判断ResultMap中是否有嵌套的ResultMap,
当使用或标签通过JOIN查询方式进行级联映射时,hasNestedResultMaps()方法的返回值为true,调用handleRowValuesForNestedResultMap()方法;
当使用和标签关联一个外部的查询Mapper时,ResultMap对象的hasNestedResultMaps属性值为false,调用handleRowValuesForSimpleResultMap()方法。
有嵌套ResultMap时的处理逻辑,handleRowValuesForNestedResultMap()方法:
对结果集对象进行遍历,处理每一行数据。
首先调用resolveDiscriminatedResultMap()方法处理标签中通过标签配置的鉴别器信息,根据字段值获取对应的ResultMap对象,
然后调用DefaultResultSetHandler类的getRowValue()方法将结果集中的一行数据转换为Java实体对象。
在getRowValue()方法中:
1.调用createResultObject()方法处理通过<constructor>
标签配置的构造器映射,根据配置信息找到对应的构造方法,然后通过MyBatis中的ObjectFactory创建ResultMap关联的实体对象。
2.调用applyAutomaticMappings()方法处理自动映射,对未通过<result>
标签配置映射的数据库字段进行与Java实体属性的映射处理。
方法中首先获取未指定映射的所有数据库字段和对应的Java属性,然后获取对应的字段值,通过反射机制为Java实体对应的属性值赋值。
3.调用applyPropertyMappings()方法处理<result>
标签配置的映射信息。对所有<result>
标签配置的映射信息进行遍历,然后找到数据库字段对应的值,为Java实体属性赋值。applyPropertyMappings()方法的实现代码如下:
4.调用DefaultResultSetHandler类的applyNestedResultMappings()方法处理嵌套的结果集映射。applyNestedResultMappings()方法实现如下:
在applyNestedResultMappings()方法中:
1.首先获取嵌套ResultMap对象,
2.然后根据嵌套ResultMap的Id从缓存中获取嵌套ResultMap对应的Java实体对象,如果能获取到,则调用linkObjects()方法将嵌套Java实体与外部Java实体进行关联。
如果缓存中没有,则调用getRowValue()方法创建嵌套ResultMap对应的Java实体对象并进行属性映射,然后调用linkObjects()方法与外部的Java实体对象进行关联。
没有嵌套的ResultMap,handleRowValuesForSimpleResultMap()方法:
1.首先调用skipRows()方法跳过RowBounds对象指定偏移的行,
2.然后遍历结果集中所有的行,对标签配置的鉴别器进行处理,获取实际映射的ResultMap对象,
3.接着调用getRowValue()方法处理一行记录,将数据库行记录转换为Java实体对象。getRowValue()方法实现如下:
在getRowValue():
1.创建ResultLoaderMap对象,该对象用于存放懒加载的属性及对应的ResultLoader对象,MyBatis中的ResultLoader用于执行一个查询Mapper,然后将执行结果赋值给某个实体对象的属性。
2.调用createResultObject()方法创建ResultMap对应的Java实体对象,我们需要重点关注该方法的实现,代码如下:
createResultObject()方法中:
1.首先调用重载的createResultObject()方法使用ObjectFactory对象创建Java实体对象,
2.然后判断ResultMap中是否有嵌套的查询,如果有嵌套的查询并且开启了懒加载机制,则通过MyBatis中的ProxyFactory创建实体对象的代理对象。
ProxyFactory接口有两种不同的实现,分别为CglibProxyFactory和JavassistProxyFactory。
也就是说,MyBatis同时支持使用Cglib和Javassist创建代理对象,具体使用哪种策略创建代理对象,可以在MyBatis主配置文件中通过proxyFactory属性指定。
3.调用applyAutomaticMappings()方法处理自动映射,
4.调用applyPropertyMappings()方法处理标签配置的映射字段,该方法中除了为Java实体属性设置值外,还将指定了懒加载的属性添加到ResultLoaderMap对象中。