mybatis resultMap 加载过程详解 矫情吗;* 2022-05-19 04:47 154阅读 0赞 目录 * 流程分析 * 遍历resultMap根节点 * 重载的resultMapElement * 子标签解析 * 其他流程-普通标签 id|result 标签 * buildResultMappingFromContext * buildResultMapping * 构造流程 constructor 标签 * 其他流程-嵌套标签 - association 标签 | collection 标签 * association 用法 * collection 用法 * association | collection 流程分析 * 嵌套标签解析 * 嵌套多列处理 * 辨别器流程 discriminator 标签 * 基本使用 * 理论介绍 * 源码分析 * 根标签解析 * 详解 * 构造器多参详解 * 方案3 idea设置 * 方案4 黑科技 * 总结 mybatis 有`9`大标签,分别是: \- cache-ref \- cache \- parameterMap \- resultMap \- sql \- select \- insert \- update \- delete 其中 `parameterMap` 已被官网标记为[废弃][Link 1] `cache-ref` 和 `cache` 在这个电商横行,分布式肆虐的年代,也不使用 mybatis 缓存了 那么常用的只有其中6种 \- resultMap \- sql \- select \- insert \- update \- delete 本节我们针对`resultMap`进行详细分析 分析结束之后我们可以解答内心的一些疑问: 1. resultMap 有哪些子标签,都用来干什么 2. collection 标签为什么`有时候`要自己初始化(new ArrayList<>()) # 流程分析 # 先从 XMLMapperBuilder 的 configurationElement 说起, 这里存放着9大标签的解析 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); // cache-ref 标签解析 cacheRefElement(context.evalNode("cache-ref")); // cache 标签解析 cacheElement(context.evalNode("cache")); // parameterMap 标签解析 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // resultMap 标签解析 resultMapElements(context.evalNodes("/mapper/resultMap")); // sql 标签解析 sqlElement(context.evalNodes("/mapper/sql")); // select|insert|update|delete 标签解析 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); } } ## 遍历resultMap根节点 ## private void resultMapElements(List<XNode> list) throws Exception { for (XNode resultMapNode : list) { try { resultMapElement(resultMapNode); } catch (IncompleteElementException e) { // ignore, it will be retried } } } private ResultMap resultMapElement(XNode resultMapNode) throws Exception { return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList()); } 首先进入第一道开胃菜: 1. resultMapElements, 它遍历了mapper下面所有的resultMap标签,然后每个都进行 resultMapElement 处理 2. resultMapElement 传入一个空的集合对象,调用重载 resultMapElement 为什么要传入一个空的集合对象呢? 那是因为`resultMap`标签可以嵌套,但是根节点就是空的 ## 重载的resultMapElement ## private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception { // 保存当前上下文,用于异常信息回溯 ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); // 获得 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 的 id String extend = resultMapNode.getStringAttribute("extends"); // 获得 是否自动映射 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(); // 遍历子节点,依次解析为 ResultMapping 对象,并添加到集合 resultMappings 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)); } } ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try { // 解析验证,最终添加到 configuration return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } } 看似很多,实际上就是两步 1. 遍历`解析子标签`为 ResultMapping 对象 2. `解析根标签` 为一个ResultMap并添加到configuration # 子标签解析 # 这里又分三种情况 1. constructor 子标签解析 2. discriminator 子标签解析 3. 其他标签解析 那resultMap 的子标签又有哪些,其他标签都包含哪些呢? <table> <tbody> <tr> <td>名称</td> <td>类型</td> </tr> <tr> <td>constructor</td> <td>constructor</td> </tr> <tr> <td>discriminator</td> <td>discriminator</td> </tr> <tr> <td>id</td> <td>其他</td> </tr> <tr> <td>result</td> <td>其他</td> </tr> <tr> <td>association</td> <td>其他->嵌套</td> </tr> <tr> <td>collection</td> <td>其他->嵌套</td> </tr> </tbody> </table> ## 其他流程-普通标签 id|result 标签 ## Xml 例子 <resultMap id="BlogMap" type="com.aya.mapper.Blog"> <id column="id" property="id"/> </resultMap> (id|result|association|collection)标签解析流程 List<ResultFlag> flags = new ArrayList<ResultFlag>(); // id 标签加入 flags, 其他的就不加入这个标签 if ("id".equals(resultChild.getName())) { flags.add(ResultFlag.ID); } resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); 这是最简单最常用的一种解析方式. 1. 添加标识 ID 2. 使用 buildResultMappingFromContext 构建成一个 ResultMapping 添加到集合 resultMappings ### buildResultMappingFromContext ### private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception { String property; // constructor 标签用name当做属性 if (flags.contains(ResultFlag.CONSTRUCTOR)) { property = context.getStringAttribute("name"); } else { // 普通标签 标签用property当做属性 property = context.getStringAttribute("property"); } // 废话开始 String column = context.getStringAttribute("column"); String javaType = context.getStringAttribute("javaType"); String jdbcType = context.getStringAttribute("jdbcType"); String nestedSelect = context.getStringAttribute("select"); // processNestedResultMappings 嵌套解析在 association|collection 讲解 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); // 废话结束 //构建 ResultMapping return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); } 对于简单的id|result标签解析而言,这里面大都是将xml解析成具体的数据 这里要明白两点 1. 每一个 `id|result|association|collection` 标签,都对应着一个 `ResultMapping` 对象 2. ResultMapping 是通过`builderAssistant.buildResultMapping` 构建的 ### buildResultMapping ### public ResultMapping buildResultMapping( Class<?> resultType, String property, String column, Class<?> javaType, JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix, Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags, String resultSet, String foreignColumn, boolean lazy) { // 获得 <id column="id" property="id"/> 节点的 typeHandler 的值,本例未定义,结果为 null Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType); //根据typeHandlerClass获得对象,结果为 null TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler); // 嵌套查询时解析为多个字段,例如: <association property="title" column="title={title},content={content}" .... List<ResultMapping> composites = parseCompositeColumnName(column); return new ResultMapping.Builder(configuration, property, column, javaTypeClass) .jdbcType(jdbcType) .nestedQueryId(applyCurrentNamespace(nestedSelect, true)) .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)) .resultSet(resultSet) .typeHandler(typeHandlerInstance) .flags(flags == null ? new ArrayList<ResultFlag>() : flags) .composites(composites) .notNullColumns(parseMultipleColumnNames(notNullColumn)) .columnPrefix(columnPrefix) .foreignColumn(foreignColumn) .lazy(lazy) .build(); } 这里终于找到终点了 `ResultMapping.Builder` 是一个很标准的`建造者模式`。 符合`建造者模式`的核心思想。 1. 必要的参数使用构造方法传入 2. 可选的设置使用 withXXX 设置 3. build 之后对象不可变 在build 的时候,做了一个默认的类型解析处理 public ResultMapping build() { // lock down collections // 内置标识 不可更改 resultMapping.flags = Collections.unmodifiableList(resultMapping.flags); // 组合列参数不可更改 <association column="title={title},content={content}" .... resultMapping.composites = Collections.unmodifiableList(resultMapping.composites); // 设置默认的类型处理器 resolveTypeHandler(); // 验证嵌套的信息有效 validate(); // 返回构建成功的 resultMapping // resultMapping 是成员变量 return resultMapping; } private void resolveTypeHandler() { // 用户没有在标签定义 typeHandler时,使用mybatis默认的typeHandler if (resultMapping.typeHandler == null && resultMapping.javaType != null) { Configuration configuration = resultMapping.configuration; TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); resultMapping.typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType); } } ## 构造流程 constructor 标签 ## 一个参数只要加入 javaType 就可以匹配到具体的构造函数了,不加`javaType`时,默认匹配 ctor(Object) 的构造 <resultMap id="BlogMap" type="com.aya.mapper.Blog" > <constructor> <idArg column="pid" javaType="integer"/> </constructor> </resultMap> 这里走流程 `if ("constructor".equals(resultChild.getName()))` 的代码块,分析 `processConstructorElement` 的内部 private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception { List<XNode> argChildren = resultChild.getChildren(); for (XNode argChild : argChildren) { List<ResultFlag> flags = new ArrayList<ResultFlag>(); flags.add(ResultFlag.CONSTRUCTOR); if ("idArg".equals(argChild.getName())) { flags.add(ResultFlag.ID); } resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags)); } } 将 constructor 的每个子标签构建成 ResultMapping 对象,添加到 resultMappings 也就是两个步骤 1. 添加标识 ResultFlag.CONSTRUCTOR ,构建 ResultMapping 2. 添加子标签 ,每个 ResultMapping 都有标识`ResultFlag.CONSTRUCTOR` ## 其他流程-嵌套标签 - association 标签 | collection 标签 ## ### association 用法 ### association 是一个对象的解析器,有两种用法 <select id="selectContent" resultType="string"> SELECT content FROM blog WHERE id = #{id} </select> <resultMap id="BlogMap" type="com.aya.mapper.Blog" > <!-- 直接使用对象,在result为对象赋值 --> <association property="content" javaType="string" > <result column="content" /> </association> <!-- 使用 select 引用的查询结果,可以通过column传参 --> <!-- 多参 key 是select引用需要用到变量,value是这边查询到的值 {id=id,content=content} --> <association property="contentSelect" column="id" select="selectContent" javaType="string" /> </resultMap> public class Blog { private String content; private String contentSelect; //省略 getter , setter } ### collection 用法 ### collection 是一个集合的解析器,有两种用法 <select id="selectContentList" resultType="string"> SELECT content FROM blog WHERE id = #{id} </select> <resultMap id="BlogMap" type="com.aya.mapper.Blog" > <!-- 直接使用集合, 那么bean的集合必须初始化 --> <collection property="contentList" javaType="string" > <result column="content" /> </collection> <!-- 通过select 获取的集合一定不会为null. 所以bean的定义可以不用初始化 --> <collection property="contentListSelect" column="id" select="selectContentList" javaType="list" /> </resultMap> public class Blog { public Blog() { } // 必须初始化 private List<String> contentList = new ArrayList<>(); // 可以不用初始化 private List<String> contentListSelect; //省略 getter , setter } ### association | collection 流程分析 ### 这里走的流程是其他标签解析的流程,但是比普通的标签有额外的两个方法要详细说明 在前面讲 buildResultMappingFromContext 的分析的时候,在进行 resultMap 获取是,有一个嵌套的处理。 就是专门用来处理具有嵌套效果的标签的, 而`association | collection` 刚好就是嵌套标签。 嵌套标签解析 String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.<ResultMapping> emptyList())); 嵌套多列处理 List<ResultMapping> composites = parseCompositeColumnName(column); ### 嵌套标签解析 ### private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception { if ("association".equals(context.getName()) || "collection".equals(context.getName()) || "case".equals(context.getName())) { if (context.getStringAttribute("select") == null) { ResultMap resultMap = resultMapElement(context, resultMappings); return resultMap.getId(); } } return null; } 这里做的就是当 `association|collection|case` 标签没有属性`select`的时候,递归解析子标签 ### 嵌套多列处理 ### private List<ResultMapping> parseCompositeColumnName(String columnName) { List<ResultMapping> composites = new ArrayList<ResultMapping>(); if (columnName != null && (columnName.indexOf('=') > -1 || columnName.indexOf(',') > -1)) { StringTokenizer parser = new StringTokenizer(columnName, "{}=, ", false); //例如 column="{columnKeyA=columnValueA,columnKeyB=columnValueB}" 分两次遍历,添加到composites while (parser.hasMoreTokens()) { String property = parser.nextToken(); String column = parser.nextToken(); ResultMapping complexResultMapping = new ResultMapping.Builder( configuration, property, column, configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build(); composites.add(complexResultMapping); } } return composites; } 这里就是来解析标签中 `column="{columnKeyA=columnValueA,columnKeyB=columnValueB}"` 部分的代码 装配成`ResultMapping`集合,在最后build里面的validate进行验证 validate 验证嵌套 // Issue #4 and GH #39: column is optional only in nested resultmaps but not in the rest if (resultMapping.nestedResultMapId == null && resultMapping.column == null && resultMapping.composites.isEmpty()) { throw new IllegalStateException("Mapping is missing column attribute for property " + resultMapping.property); } 也就是说,你不可以写成以下形式 <collection property="contentList" javaType="string" > <!-- 可以没有 result 标签,会导致查询结果错误--> <!-- result标签没有column会导致装配ResultMap对象失败,抛出异常 IllegalStateException --> <result /> </collection> ## 辨别器流程 discriminator 标签 ## ### 基本使用 ### <resultMap id="BlogMap" type="com.aya.mapper.Blog" > <result column="pid" property="id"/> <discriminator javaType="string" column="pid" > <case value="1" resultType="com.aya.mapper.Blog"> <result column="ptitle" property="title"/> </case> <case value="2" resultType="com.aya.mapper.Blog"> <result column="pcontent" property="title"/> </case> </discriminator> </resultMap> <!-- 防止属性自动填充 --> <select id="selectAll" resultMap="BlogMap" > select id as pid,title as ptitle,content as pcontent from blog </select> 实体类 public class Blog { private String id; private String title; //省略 getter , setter } 查询结果 [Blog{id='1', title='标题'}, Blog{id='2', title='content2'}, Blog{id='3', title='null'}] ### 理论介绍 ### discriminator 是一个辨别器. 用`switch...case`的方式决定让整个resultMap返回什么类型 也就是说,加载的时候只能是某个动态对象,执行查询操作后才知道实际的返回类型. 因为是`switch...case`,所以不能加入条件判断 discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); discriminator 的判断的地方可以发现, 如果有`discriminator`,那么有且仅有一个. 接下来就分析`Discriminator`对象的创建过程 ### 源码分析 ### 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()) { // case 的value String value = caseChild.getStringAttribute("value"); //processNestedResultMappings 解析嵌套的ResultMapping,并返回创建的名称 String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings)); discriminatorMap.put(value, resultMap); } // 构建 Discriminator return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap); } 1. 每个value都对应着一个`ResultMapping`名称,在根据查询结果进行转换 2. 使用构建助手构建辨别器 public Discriminator buildDiscriminator( Class<?> resultType, String column, Class<?> javaType, JdbcType jdbcType, Class<? extends TypeHandler<?>> typeHandler, Map<String, String> discriminatorMap) { // 将辨别器节点构建成一个 ResultMapping ResultMapping resultMapping = buildResultMapping( resultType, null, column, javaType, jdbcType, null, null, null, null, typeHandler, new ArrayList<ResultFlag>(), null, null, false); Map<String, String> namespaceDiscriminatorMap = new HashMap<String, String>(); //构建辨别器的所有子节点 for (Map.Entry<String, String> e : discriminatorMap.entrySet()) { String resultMap = e.getValue(); // 使用将简写改为全称 resultMap = applyCurrentNamespace(resultMap, true); namespaceDiscriminatorMap.put(e.getKey(), resultMap); } // 用建造者模式创建构建器 return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build(); 这里就通过 Discriminator 的建造者创建了对象. Discriminator 是一个根据结果动态选择返回类型的一个标签,单纯的分析解析过程,意义不大 # 根标签解析 # public ResultMap resolve() { return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping); } 通过上一层的 builderAssistant.addResultMap ,实际上添加到哪里去了呢? 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("Could not find a parent resultmap with id '" + extend + "'"); } ResultMap resultMap = configuration.getResultMap(extend); List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(resultMap.getResultMappings()); extendedResultMappings.removeAll(resultMappings); // Remove parent constructor if this resultMap declares a constructor. 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(); } } } resultMappings.addAll(extendedResultMappings); } //ResultMap.Builder 构建 ResultMap. 根标签 resultMap ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping) .discriminator(discriminator) .build(); // 将根标签解析的对象resultMap添加到configuration configuration.addResultMap(resultMap); return resultMap; } 实际上那么多的解析从宏观上就只有两步 1. 将resultMap的所有子标签解析成ResultMapping对象 2. 将resultMap标签构建成ResultMap对象添加到configuration # 详解 # ## 构造器多参详解 ## <resultMap id="BlogMap" type="com.aya.mapper.Blog" > <constructor> <idArg column="pid" name="id" javaType="integer"/> <arg column="ptitle" name="title" javaType="string"/> </constructor> </resultMap> public class Blog implements Serializable{ private Integer id; private String title; public Blog(Integer id, String title) { this.id = id; this.title = title; } } 以上面的代码为例,查询时在创建ResultMap时,抛出异常 ### 方案3 idea设置 ### 打开File => Settings 依次选择 Buld,Execution,Deployment => Compiler => Java Compiler 设置 Additional command line parameters: `-parameters` ### 方案4 黑科技 ### **切勿使用** 黑科技: 既然不不开启特性,又想对应,那我就满足你的原理 <resultMap id="BlogMap" type="com.aya.mapper.Blog" > <constructor> <idArg column="pid" name="arg0" javaType="integer"/> <arg column="ptitle" name="arg1" javaType="string"/> </constructor> </resultMap> # 总结 # 1. resultMap 有6个子标签 2. select属性关联集合不会为null [Link 1]: http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html
还没有评论,来说两句吧...