MyBatis其中一个比较强大的功能是支持查询结果级联映射。使用MyBatis级联映射,我们可以很轻松地实现一对多、一对一或者多对多关联查询,甚至可以利用MyBatis提供的级联映射实现懒加载。所谓的懒加载,就是当我们在一个实体对象中关联其他实体对象时,如果不需要获取被关联的实体对象,则不需要为被关联的实体执行额外的查询操作,仅当调用当前实体的Getter方法获取被关联实体对象时,才会执行一次额外的查询操作。通过这种方式在一定程度上能够减轻数据库的压力。本章我们就来学习一下MyBatis的级联映射和懒加载机制以及它的实现原理。
MyBatis懒加载机制
我们知道MyBatis的级联映射可以通过两种方式实现,其中一种方式是为实体的属性关联一个外部的查询Mapper,这种情况下,MyBatis实际上为实体的属性执行一次额外的查询操作;另一种方式是通过JOIN查询来实现,这种方式需要为实体关联的其他实体对应的属性配置映射关系,通过JOIN查询方式只需要一次查询即可。
在一些情况下,我们需要按需加载,即当我们查询用户信息时,如果不需要获取用户订单信息,则不需要执时订单查询对应的Mapper,仅当调用Getter方法获取订单数据时,才执行一次额外的查询操作。这种方式能够在一定程度上能够减少数据库IO次数,提升系统性能。
MyBatis中提供了懒加载机制,能够帮助我们实现这种需求,接下来我们就来了解一下MyBatis懒加载机制的使用方式。MyBatis主配置文件中提供了lazyLoadingEnabled和aggressiveLazyLoading参数用来控制是否开启懒加载机制。lazyLoadingEnabled参数值为true时表示开启懒加载,否则表示不开启懒加载。aggressiveLazyLoading参数用于控制ResultMap默认的加载行为,参数值为false表示ResultMap默认的加载行为为懒加载,否则为积极加载。
除此之外,<collection>和<association>标签还提供了一个fetchType属性,用于控制级联查询的加载行为,fetchType属性值为lazy时表示该级联查询采用懒加载方式,当fetchType属性值为eager时表示该级联查询采用积极加载方式。
我们可以使用如下配置开启懒加载:
<settings>
<!-- 打开延迟加载的开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为懒加载即按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- equals,clone,hashCode,toString等方法不触发懒加载 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
我们可以使用如下测试用例测试MyBatis的懒加载机制:
执行测试用例时,控制台输出内容如下。从控制台日志输出内容可以看出,开启懒加载机制后,当我们调用OrderMapper的getOrderByNo()方法查询订单信息时,Order实体user属性关联的外部Mapper并没有被执行,当我们调用Order对象的getUser()方法获取订单对应的用户信息时,才会执行Order实体user属性关联的Mapper查询用户信息。
MyBatis级联映射实现原理
由于MyBatis中的ResultMap是实现级联映射和懒加载机制的基础,因此在介绍MyBatis级联映射源码之前,我们首先需要对MyBatis中的ResultMap有较为详细的了解。
MyBatis是一个半自动化的ORM框架,可以将数据库中的记录转换为Java实体对象,但是Java实体属性通常采用驼峰命名法,而数据库字段习惯采用下画线分割命名法,因此需要用户指定Java实体属性与数据库表字段之间的映射关系。
MyBatis的Mapper配置中提供了一个<resultMap>标签,用于建立数据库字段与Java实体属性之间的映射关系。下面是一个简单的ResultMap配置:
<resultMap id="joinedAuthor" type="org.apache.ibatis.domain.blog.Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
<result property="favouriteSection" column="author_favourite_section"/>
</resultMap>
如上面的配置所示,每个ResultMap需要有一个全局唯一的Id,由<resultMap>标签的id属性指定。除此之外,ResultMap还需要通过type属性指定与哪一个Java实体进行映射。在<resultMap>标签中,需要使用<id>或<result>标签配置具体的某个表字段与Java实体属性之间的映射关系。数据库主键通常使用<id>标签建立映射关系,普通数据库字段则使用<reuslt>标签。
除了属性映射外,ResultMap还支持使用构造器映射,构造器映射需要使用<constructor>标签。下面是构造器映射案例,配置如下:
<resultMap id="selectImmutableAuthor" type="org.apache.ibatis.domain.blog.ImmutableAuthor">
<constructor>
<idArg column="id" javaType="_int" />
<arg column="username" javaType="string" />
<arg column="password" javaType="string" />
<arg column="email" javaType="string" />
<arg column="bio" javaType="string" />
</constructor>
<result property="favouriteSection" column="favourite_section" javaType="org.apache.ibatis.domain.blog.Section" />
</resultMap>
如上面的配置所示,使用构造器映射的前提是建立映射的Java实体需要提供对应的构造方法。<idArg>标签用于配置数据库主键的映射,<arg>标签用于配置普通数据库字段的映射。
最后我们来总结一下,<resultMap>标签中可以使用下面几种子标签。
- <constructor>:该标签用于建立构造器映射。该标签有两个子标签,<idArg>标签用于配置主键映射,标记出主键,可以提高整体性能;<arg>标签用于配置普通字段的映射。
- <id>:用于配置数据库主键映射,标记出数据库主键,有助于提高整体性能。
- <result>:用于配置数据库字段与Java实体属性之间的映射关系。
- <association>:用于配置一对一关联映射,可以关联一个外部的查询Mapper或者配置一个嵌套的ResultMap。
- <collection>:用于配置一对多关联映射,可以关联一个外部的查询Mapper或者配置一个嵌套的ResultMap。
- <discriminator>:用于配置根据字段值使用不同的ResultMap。该标签有一个子标签,<case>标签用于枚举字段值对应的ResultMap,类似于Java中的switch语法。
ResultMap解析过程
MyBatis在启动时,所有配置信息都会被转换为Java对象,通过<resultMap>标签配置的结果集映射信息也不例外。MyBatis通过ResultMap类描述<resultMap>标签的配置信息,ResultMap类的所有属性如下:
public class ResultMap {
/**
* 配置对象
*/
private Configuration configuration;
/**
* ID
*/
private String id;
/**
* 类型
*/
private Class<?> type;
/**
* 结果映射列表
*/
private List<ResultMapping> resultMappings;
/**
* ID结果映射列表
*/
private List<ResultMapping> idResultMappings;
/**
* 构造函数结果映射列表
*/
private List<ResultMapping> constructorResultMappings;
/**
* 属性结果映射列表
*/
private List<ResultMapping> propertyResultMappings;
/**
* 映射的列集合
*/
private Set<String> mappedColumns;
/**
* 映射的属性集合
*/
private Set<String> mappedProperties;
/**
* 判断字段是否为鉴别器
*/
private Discriminator discriminator;
/**
* 是否有嵌套结果映射
*/
private boolean hasNestedResultMaps;
/**
* 是否有嵌套查询
*/
private boolean hasNestedQueries;
/**
* 是否自动映射
*/
private Boolean autoMapping;
这些属性的含义如下。
- 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>标签解析生成ResultMap对象的过程。MyBatis中的Mapper配置信息解析都是通过XMLMapperBuilder类完成的,该类提供了一个parse()方法,用于解析Mapper中的所有配置信息,代码如下:
/**
* 解析方法。
*/
public void parse() {
/**
* 如果资源还没有加载过,则执行以下操作:
* 1. 获取配置元素,传入 mapper 的根节点
* 2. 将资源添加到配置的已加载资源列表中
* 3. 绑定命名空间对应的映射器
*/
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
/**
* 解析待处理的结果映射
*/
parsePendingResultMaps();
/**
* 解析待处理的缓存引用
*/
parsePendingCacheRefs();
/**
* 解析待处理的语句
*/
parsePendingStatements();
}
如上面的代码所示,在XMLMapperBuilder的parse()方法中,调用XMLMapperBuilder类的configurationElement()进行处理,该方法定义如下:
/**
* 处理配置元素
*
* @param context XML节点
*/
private void configurationElement(XNode context) {
try {
// 获取命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper的命名空间不能为空");
}
// 设置当前命名空间
builderAssistant.setCurrentNamespace(namespace);
// 处理缓存引用元素
cacheRefElement(context.evalNode("cache-ref"));
// 处理缓存元素
cacheElement(context.evalNode("cache"));
// 处理参数映射元素
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 处理结果映射元素
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 处理SQL元素
sqlElement(context.evalNodes("/mapper/sql"));
// 根据上下文构建语句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("解析Mapper XML时发生错误。XML的位置是'" + resource + "',原因:" + e, e);
}
}
在XMLMapperBuilder类的configurationElement()方法中,调用resultMapElements()方法对所有<resultMap>标签进行解析。resultMapElements()方法最终会调用重载的resultMapElement()方法对每个<resultMap>标签进行解析,该方法代码如下:
/**
* 根据给定的结果映射节点生成结果映射元素
*
* @param resultMapNode 结果映射节点
* @param additionalResultMappings 额外的结果映射
* @param enclosingType 包含的类型
* @return 结果映射元素
*/
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
如上面的代码所示,在XMLMapperBuilder类的resultMapElement()方法中,首先获取<resultMap>标签的所有属性信息,然后对<id>、<constructor>、<discriminator>子标签进行解析,接着创建一个ResultMapResolver对象,调用ResultMapResolver对象的resolve()方法返回一个ResultMap对象。ResultMapResolver对象的resolve()方法代码如下:
/**
* 解析结果映射
*
* @return 解析结果
*/
public ResultMap resolve() {
return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}
如上面的代码所示,ResultMapResolver对象的resolve()方法的逻辑非常简单,调用MapperBuilderAssistant对象的addResultMap()方法创建ResultMap对象,并把ResultMap对象添加到Configuration对象中。MapperBuilderAssistant的addResultMap()方法代码如下:
/**
* 添加一个结果映射到配置中
*
* @param id 结果映射的id
* @param type 结果映射关联的类
* @param extend 父结果映射的id
* @param discriminator 分类歧视字段
* @param resultMappings 结果映射的集合
* @param autoMapping 是否自动映射
* @return 结果映射对象
*/
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("无法找到id为'" + extend + "'的父结果映射");
}
ResultMap resultMap = configuration.getResultMap(extend);
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
extendedResultMappings.removeAll(resultMappings);
// 如果当前结果映射声明了构造函数,则移除父结果映射中所有的构造函数参数
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
resultMappings.addAll(extendedResultMappings);
}
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
configuration.addResultMap(resultMap);
return resultMap;
}
在MapperBuilderAssistant类的addResultMap()方法中,首先判断该ResultMap是否继承了其他ResultMap。如果是,则获取父ResultMap对象,然后去除父ResultMap中的构造器映射信息,将父ResultMap中配置的映射信息添加到当前ResultMap对象,最后通过建造者模式创建ResultMap对象。在ResultMap.Builder类中创建了一个ResultMap对象,然后为ResultMap对象的所有属性赋值。