MyBatis 解析xml配置的mapper - litter-fish/ReadSource GitHub Wiki
创建XMLMapperBuilder对象
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
// 创建Mapper 构建助手
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
// 创建Mapper 构建助手
public MapperBuilderAssistant(Configuration configuration, String resource) {
super(configuration);
ErrorContext.instance().resource(resource);
this.resource = resource;
}
// super
public BaseBuilder(Configuration configuration) {
//
this.configuration = configuration;
// 设置类型别名注册器
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
// 类型处理器注册器
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
委托 XMLMapperBuilder 进行解析
// --XMLMapperBuilder
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper子节点
configurationElement(parser.evalNode("/mapper"));
// 解析完后将文件加入configuration对象已经解析的属性集合中
configuration.addLoadedResource(resource);
// 绑定命名空间
bindMapperForNamespace();
}
// 解析待定的ResultMap
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
解析 mapper 标签
// --XMLMapperBuilder
private void configurationElement(XNode context) {
try {
// 获取名称空间,然后校验,确保不为空
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置构建助手的当前处理名称空间
builderAssistant.setCurrentNamespace(namespace);
// 解析对其他命名空间缓存配置的引用。
cacheRefElement(context.evalNode("cache-ref"));
// 解析对给定命名空间的缓存配置。
cacheElement(context.evalNode("cache"));
// 已被废弃!老式风格的参数映射。更好的办法是使用内联参数,此元素可能在将来被移除。
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析描述如何从数据库结果集中来加载对象
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 可被其他语句引用的可重用语句块。
sqlElement(context.evalNodes("/mapper/sql"));
// 解析映射插入、查询、更新、删除语句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
解析cache-ref子节点
在多个命名空间中共享相同的缓存配置和实例。
解析时序图
// --XMLMapperBuilder
/**
* 在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。
* @param context
*/
private void cacheRefElement(XNode context) {
if (context != null) {
// 将当前处理的名称空间加入configuration对象的cacheRefMap map集合中
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
// 创建 cache-ref 解析器 CacheRefResolver
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 委托CacheRefResolver解析 cache-ref节点
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
/**
* 缓存参考因为通过namespace指向其他的缓存。所以会出现第一次解析的时候指向的缓存还不存在的情况,所以需要在所有的mapper文件加载完成后进行二次处理,
* 不仅仅是缓存参考,其他的CRUD也一样。所以在XMLMapperBuilder.configuration中有很多的incompleteXXX,这种设计模式类似于JVM GC中的mark and sweep,
* 标记、然后处理。所以当捕获到IncompleteElementException异常时,没有终止执行,而是将指向的缓存不存在的cacheRefResolver添加到
* configuration.incompleteCacheRef中。
*/
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
首先会将当前处理的名称空间加入configuration对象的cacheRefMap map集合中, 接着创建 cache-ref 解析器 CacheRefResolver, 最后委托CacheRefResolver解析 cache-ref节点。
// org/apache/ibatis/builder/CacheRefResolver.java
public Cache resolveCacheRef() {
return assistant.useCacheRef(cacheRefNamespace);
}
委托mapper构建助手进行处理
// org/apache/ibatis/builder/MapperBuilderAssistant.java
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
// 首先查询caches集合是否存在其引用
Cache cache = configuration.getCache(namespace);
if (cache == null) {
// 不存在的抛异常,cacheRefElement中会加入incompleteCacheRefs集合中进行二次解析
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
对于解析过程中如果发现Cache中不存在对应的引用,将抛出IncompleteElementException异常,然后加入configuration对象的incompleteCacheRef集合中
解析cache子节点
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
cache解析时序图
源码解释:
// --XMLMapperBuilder
/**
* 默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
* 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。
*
* 映射语句文件中的所有 select 语句的结果将会被缓存。
* 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
* 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
* 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
* 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
* 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
*
* 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。??????
*/
private void cacheElement(XNode context) throws Exception {
if (context != null) {
// 默认情况下,mybatis使用的是永久缓存PerpetualCache
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 获取清除策略
/**
* LRU – 最近最少使用:移除最长时间不被使用的对象。
* FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
* SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
* WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
*/
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
Long flushInterval = context.getLongAttribute("flushInterval");
// size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024
Integer size = context.getIntAttribute("size");
// readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。
// 而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 调用builderAssistant.useNewCache构建缓存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
首先获取节点的属性,type、eviction,通过属性名获取相应的class对象。 接着委托Mapper创建助手进行缓存的创建。 缓存的构建 builderAssistant.useNewCache
// --MapperBuilderAssistant
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 构建Cache 实例
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class)) // 二级缓存默认使用:PerpetualCache
.addDecorator(valueOrDefault(evictionClass, LruCache.class)) // 默认装饰器:LruCache
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 将二级缓存配置设置到configuration实例中
configuration.addCache(cache);
currentCache = cache;
return cache;
}
cache构建模式的分析
构建Cache 实例
// --CacheBuilder
public Cache build() {
// 设置实现类及装饰类
setDefaultImplementations();
// 创建实现类
Cache cache = newBaseCacheInstance(implementation, id);
// 设置缓存的属性
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
// 给缓存加上装饰器
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 给缓存加上一些功能
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
设置实现类及装饰类
private void setDefaultImplementations() {
if (implementation == null) {
// 设置永久缓存
implementation = PerpetualCache.class;
// 如果无装饰类则添加LruCache 清除策略 装饰类
if (decorators.isEmpty()) {
decorators.add(LruCache.class);
}
}
}
反射方式创建缓存实现类
// org/apache/ibatis/mapping/CacheBuilder.java
private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
try {
return cacheConstructor.newInstance(id);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
}
}
反射方式给缓存添加装饰器
private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
Constructor<? extends Cache> cacheConstructor = getCacheDecoratorConstructor(cacheClass);
try {
return cacheConstructor.newInstance(base);
} catch (Exception e) {
throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e);
}
}
给缓存增加装饰类
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
// 加强缓存的定时刷新功能
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
// 加强缓存的可序列化操作
cache = new SerializedCache(cache);
}
// 加强缓存的日志功能
cache = new LoggingCache(cache);
// 加强缓存的同步功能,解决线程安全问题
cache = new SynchronizedCache(cache);
if (blocking) {
// 加强缓存的阻塞功能
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
最终构造的缓存实例
结果集 resultMap 解析
ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。
复杂的结果映射例子
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" javaType="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"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>
结果映射(resultMap)
-
constructor - 用于在实例化类时,注入结果到构造方法中 idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能 arg - 将被注入到构造方法的一个普通结果
-
id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
-
result – 注入到字段或 JavaBean 属性的普通结果
-
association – 一个复杂类型的关联;许多结果将包装成这种类型 嵌套结果映射 – 关联本身可以是一个 resultMap 元素,或者从别处引用一个
-
collection – 一个复杂类型的集合 嵌套结果映射 – 集合本身可以是一个 resultMap 元素,或者从别处引用一个
-
discriminator – 使用结果值来决定使用哪个 resultMap case – 基于某些值的结果映射 嵌套结果映射 – case 本身可以是一个 resultMap 元素,因此可以具有相同的结构和元素,或者从别处引用一个
resultMap解析时序图
遍历所有的 resultMap 节点进行解析
// --XMLMapperBuilder
private void resultMapElements(List<XNode> list) throws Exception {
// 遍历所有的 resultMap 节点进行解析
for (XNode resultMapNode : list) {
try {
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {}
}
}
resultMap 节点解析
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}
委托重载方法解析 resultMap 节点
// org/apache/ibatis/builder/xml/XMLMapperBuilder.java
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 当前命名空间中的一个唯一标识,用于标识一个结果映射。
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
// 类的完全限定名, 或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
String extend = resultMapNode.getStringAttribute("extends");
// 如果设置这个属性,MyBatis将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
Class<?> typeClass = resolveClass(type);
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(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<ResultFlag>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 所有的子节点都被解析到resultMappings中, 在解析完整个resultMap中的所有子元素之后,调用ResultMapResolver进行解析
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
首先获取resultMap节点的属性, 接着获取子节点,遍历子节点进行解析。
解析 resultMap 的子节点构造器 constructor, 将结果放入 resultMappings 中
resultMap 的子节点构造器 constructor的解析时序图:
// --XMLMapperBuilder
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
List<XNode> argChildren = resultChild.getChildren();
/**
* <constructor>
* <idArg column="blog_id" javaType="int"/>
* <arg column="blog_name" javaType="string"/>
* </constructor>
*/
for (XNode argChild : argChildren) {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
flags.add(ResultFlag.CONSTRUCTOR);
if ("idArg".equals(argChild.getName())) {
// 子节点名称为idArg的节点表示ID需要特殊处理
flags.add(ResultFlag.ID);
}
// 将constructor子节点 构造 ResultMapping 成实例
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
}
}
首先根据节点名称,区别是ID节点还是非ID节点,接着将constructor子节点 构造 ResultMapping 成实例
构造 ResultMapping 实例, 将结果放入 resultMappings 中
// --XMLMapperBuilder
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
property = context.getStringAttribute("property");
}
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");
// resultMap中可以包含association或collection复合类型,这些复合类型可以使用外部定义的公用resultMap或者内嵌resultMap,
// 所以这里的处理逻辑是如果有resultMap就获取resultMap,如果没有,那就动态生成一个。如果自动生成的话,他的resultMap id通过调用XNode.getValueBasedIdentifier()来获得
String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.<ResultMapping> emptyList()));
String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
String typeHandler = context.getStringAttribute("typeHandler");
String resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
首先获取参数的属性配置,然后使用这些属性构造一个ResultMapping对象,其中参数属性resultMap可以包含association或collection复合类型
嵌套resultMap的解析
// --XMLMapperBuilder
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception {
// 嵌套resultMap的解析
if ("association".equals(context.getName())
|| "collection".equals(context.getName())
|| "case".equals(context.getName())) {
if (context.getStringAttribute("select") == null) {
// 未配置复杂类型属性的映射语句的 ID,进行递归解析ResultMap节点
ResultMap resultMap = resultMapElement(context, resultMappings);
return resultMap.getId();
}
}
return null;
}
子节点 discriminator 鉴别器的解析
子节点 discriminator 鉴别器的解析时序图
子节点 discriminator 鉴别器的解析, 将结果放入 resultMappings 中
// --XMLMapperBuilder
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
// 解析属性
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String typeHandler = context.getStringAttribute("typeHandler");
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Map<String, String> discriminatorMap = new HashMap<String, String>();
// 解析case子节点
for (XNode caseChild : context.getChildren()) {
String value = caseChild.getStringAttribute("value");
// 递归解析case子节点
String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
discriminatorMap.put(value, resultMap);
}
// 创建Discriminator实例
return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
}
委托ResultMapResolver进行ResultMap的封装
// --ResultMapResolver
public ResultMap resolve() {
return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}
configuration配置中加入ResultMap解析对象
// --MapperBuilderAssistant
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
// 将id/extend填充为完整模式,也就是带命名空间前缀,true不需要和当前resultMap所在的namespace相同,比如extend和cache,否则只能是当前的namespace
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
if (extend != null) {
// 首先检查继承的resultMap是否已存在,如果不存在则标记为incomplete,会进行二次处理
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
ResultMap resultMap = configuration.getResultMap(extend);
List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(resultMap.getResultMappings());
// 剔除所继承的resultMap里已经在当前resultMap
extendedResultMappings.removeAll(resultMappings);
// Remove parent constructor if this resultMap declares a constructor.
// 如果本resultMap已经包含了构造器,则剔除继承的resultMap里面的构造器
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
Iterator<ResultMapping> extendedResultMappingsIter = extendedResultMappings.iterator();
while (extendedResultMappingsIter.hasNext()) {
if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {
extendedResultMappingsIter.remove();
}
}
}
// 都处理完成之后,将继承的resultMap里面剩下那部分不重复的resultMap子元素添加到当前的resultMap中,
// 所以这个addResultMap方法的用途在于启动时就创建了完整的resultMap,这样运行时就不需要去检查继承的映射和构造器,有利于性能提升。
resultMappings.addAll(extendedResultMappings);
}
// 构造ResultMap对象
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
configuration.addResultMap(resultMap);
return resultMap;
}
ResultMap对象属性
public class ResultMap {
private Configuration configuration;
// resultMap的id属性
private String id;
// resultMap的type属性,有可能是alias
private Class<?> type;
// resultMap下的所有节点
private List<ResultMapping> resultMappings;
// resultMap下的id节点比如<id property="id" column="user_id" />
private List<ResultMapping> idResultMappings;
// resultMap下的构造器节点<constructor>
private List<ResultMapping> constructorResultMappings;
// resultMap下的property节点比如<result property="password" column="hashed_password"/>
private List<ResultMapping> propertyResultMappings;
// 映射的列名
private Set<String> mappedColumns;
// 映射的javaBean属性名,所有映射不管是id、构造器或者普通的 TODO >?????
private Set<String> mappedProperties;
// 鉴别器
private Discriminator discriminator;
// 是否有嵌套的resultMap比如association或者collection
private boolean hasNestedResultMaps;
// 是否有嵌套的查询,也就是select属性
private boolean hasNestedQueries;
// autoMapping属性,这个属性会覆盖全局的属性autoMappingBehavior
private Boolean autoMapping;
}
ResultMapping属性
public class ResultMapping {
private Configuration configuration;
private String property;
private String column;
private Class<?> javaType;
private JdbcType jdbcType;
private TypeHandler<?> typeHandler;
private String nestedResultMapId;
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
public ResultMap build() {
if (resultMap.id == null) {
throw new IllegalArgumentException("ResultMaps must have an id");
}
resultMap.mappedColumns = new HashSet<String>();
resultMap.mappedProperties = new HashSet<String>();
resultMap.idResultMappings = new ArrayList<ResultMapping>();
resultMap.constructorResultMappings = new ArrayList<ResultMapping>();
resultMap.propertyResultMappings = new ArrayList<ResultMapping>();
final List<String> constructorArgNames = new ArrayList<String>();
for (ResultMapping resultMapping : resultMap.resultMappings) {
// 判断是否有嵌套查询, nestedQueryId是在buildResultMappingFromContext的时候通过读取节点的select属性得到的
resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
// 判断是否嵌套了association或者collection, nestedResultMapId是在buildResultMappingFromContext的时候通过读取节点的resultMap属性得到的
// 或者内嵌resultMap的时候自动计算得到的。
// 注:这里的resultSet没有地方set进来,DTD中也没有看到,不确定是不是有意预留的,但是association/collection的子元素中倒是有声明
resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps
|| (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
// 获取column属性, 包括复合列,复合列是在org.apache.ibatis.builder.MapperBuilderAssistant.parseCompositeColumnName(String)中解析的。
// 所有的数据库列都被按顺序添加到resultMap.mappedColumns中
final String column = resultMapping.getColumn();
if (column != null) {
resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
} else if (resultMapping.isCompositeResult()) {
for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
final String compositeColumn = compositeResultMapping.getColumn();
if (compositeColumn != null) {
resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
}
}
}
// 所有映射的属性都被按顺序添加到resultMap.mappedProperties中,ID单独存储
final String property = resultMapping.getProperty();
if(property != null) {
resultMap.mappedProperties.add(property);
}
// 所有映射的构造器被按顺序添加到resultMap.constructorResultMappings
// 如果本元素具有CONSTRUCTOR标记,则添加到构造函数参数列表,否则添加到普通属性映射列表resultMap.propertyResultMappings
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
resultMap.constructorResultMappings.add(resultMapping);
if (resultMapping.getProperty() != null) {
constructorArgNames.add(resultMapping.getProperty());
}
} else {
resultMap.propertyResultMappings.add(resultMapping);
}
// 如果本元素具有ID标记, 则添加到ID映射列表resultMap.idResultMappings
if (resultMapping.getFlags().contains(ResultFlag.ID)) {
resultMap.idResultMappings.add(resultMapping);
}
}
// 如果没有声明ID属性,就把所有属性都作为ID属性
if (resultMap.idResultMappings.isEmpty()) {
resultMap.idResultMappings.addAll(resultMap.resultMappings);
}
// 根据声明的构造器参数名和类型,反射声明的类,检查其中是否包含对应参数名和类型的构造器,如果不存在匹配的构造器,就抛出运行时异常,这是为了确保运行时不会出现异常
if (!constructorArgNames.isEmpty()) {
final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
if (actualArgNames == null) {
throw new BuilderException("Error in result map '" + resultMap.id
+ "'. Failed to find a constructor in '"
+ resultMap.getType().getName() + "' by arg names " + constructorArgNames
+ ". There might be more info in debug log.");
}
// 构造器参数排序
Collections.sort(resultMap.constructorResultMappings, new Comparator<ResultMapping>() {
@Override
public int compare(ResultMapping o1, ResultMapping o2) {
int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
return paramIdx1 - paramIdx2;
}
});
}
// lock down collections
// 为了避免用于无意或者有意事后修改resultMap的内部结构, 克隆一个不可修改的集合提供给用户
resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
return resultMap;
}
association元素
嵌套结果映射 – 关联本身可以是一个 resultMap 元素,或者从别处引用一个 关联(association)元素处理“有一个”类型的关系。 MyBatis 有两种不同的方式加载关联:
- 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
- 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。
元素的属性
属性 描述
property 映射到列结果的字段或属性。
javaType 一个 Java 类的完全限定名,或一个类型别名
jdbcType JDBC 类型
typeHandler 类型处理器实现类的完全限定名,或者是类型别名
- 关联的嵌套 Select 查询
属性 描述
column 数据库中的列名,或者是列的别名。使用 column="{prop1=col1,prop2=col2}" 来指定多个传递给嵌套 Select 查询语句的列名
select 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。
fetchType 有效值为 lazy 和 eager,指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled
示例:
<resultMap id="blogResult" type="Blog">
<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>
“N+1 查询问题”: 执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。
解决方法:延迟加载,将大量语句同时运行的开销分散开来。
- 关联的嵌套结果映射
元素的属性
属性 描述
resultMap 结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。
columnPrefix 当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。
notNullColumn 默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。 你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列非空时才创建一个子对 象。可以使用逗号分隔来指定多个列。默认值:未设置(unset)。
autoMapping 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。
示例:
<resultMap id="authorResult" type="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"/>
</resultMap>
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author"
resultMap="authorResult" />
<association property="coAuthor"
resultMap="authorResult"
columnPrefix="co_" />
</resultMap>
collection元素
可以使用该元素表示:private List posts; 映射嵌套结果集合到一个 List 中,可以使用集合元素。 和关联元素一样,我们可以使用嵌套 Select 查询,或基于连接的嵌套结果映射集合。
- 集合的嵌套 Select 查询
<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectPostsForBlog" resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>
“ofType” 属性。这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。 表示:“posts 是一个存储 Post 的 ArrayList 集合”
- 集合的嵌套结果映射
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>
<resultMap id="blogPostResult" type="Post">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
</resultMap>
集合的多结果集(ResultSet)
属性 描述
column 当使用多个结果集时,该属性指定结果集中用于与 foreignColumn 匹配的列(多个列名以逗号隔开),以识别关系中的父类型与子类型。
foreignColumn 指定外键对应的列名,指定的列将与父类型中 column 的给出的列进行匹配。
resultSet 指定用于加载复杂类型的结果集名字。
通过这种方式可以解决N+1问题: 存储过程执行下面的查询并返回两个结果集。第一个结果集会返回博客(Blog)的结果,第二个则返回作者(Author)的结果。
SELECT * FROM BLOG WHERE ID = #{id}
SELECT * FROM AUTHOR WHERE ID = #{id}
在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。
<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
{call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>
可以指定使用 “authors” 结果集的数据来填充 “author” 关联:
<resultMap id="blogResult" type="Blog">
<id property="id" column="id" />
<result property="title" column="title"/>
<association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="bio" column="bio"/>
</association>
</resultMap>
最终ResultMap对象
第一层ResultMap
处理后的ResultMapping内存结构
这个元素可以被用来定义可重用的 SQL 代码段,这些 SQL 代码可以被包含在其他语句中。它可以(在加载的时候)被静态地设置参数。 在不同的包含语句中可以设置不同的值到参数占位符上。比如:
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
这个 SQL 片段可以被包含在其他语句中,例如:
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>
属性值也可以被用在 include 元素的 refid 属性里或 include 元素的内部语句中,例如:
<sql id="sometable">
${prefix}Table
</sql>
<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
<include refid="someinclude">
<property name="prefix" value="Some"/>
<property name="include_target" value="sometable"/>
</include>
</select>
解析 可被其他语句引用的可重用语句SQL
// --XMLMapperBuilder
private void sqlElement(List<XNode> list) throws Exception {
if (configuration.getDatabaseId() != null) {
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
sqlFragments.put(id, context);
}
}
}