一、Mapper.xml解析
前文分析了Configuration的解析过程,本文接着前文继续分析mapper映射文件的解析过程,前文可知映射文件的解析过程是由XMLMapperBuilder来完成的,所以我们可以跟着起parse方法一探究竟。
public void parse() {
//判断是否已经加载过
if (!configuration.isResourceLoaded(resource)) {
//解析映射文件
configurationElement(parser.evalNode("/mapper"));
//添加已经加载的资源
configuration.addLoadedResource(resource);
//绑定maper对象和mapper命名空间
bindMapperForNamespace();
}
//解析未完成的结果集
parsePendingResultMaps();
//解析未完成的缓存
parsePendingCacheRefs();
//解析未完成的sql语句
parsePendingStatements();
}
configurationElement(parser.evalNode("/mapper")):
这个方法就是解析整个mapper.xml文件的入口,这里我们可以去官网查看映射文件包含哪些属性以及其含义是什么,这里就不展开。
也是针对这些属性来解析
private void configurationElement(XNode context) {
try {
//获取命名空间,也就是mapper接口的全限定名
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
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);
}
}
- 解析其它命名空间的缓存,主要是用来共享其它命名空间的缓存设置,也就是建立一个引用关系
- 解析缓存,mybatis默认是开启一级缓存,一级缓存只是针对同一个SqlSession实例,不同的SqlSession实例就需要配置二级缓存,也就是只要在映射文件上加上即可,可以缓存所有的select语句,然后增删改会刷新缓存,缓存的清楚策略有LRU最少使用,FIFO根据对象进入缓存的顺序来清楚,SOFT引用,虚拟机内存不足的时候会被清楚,最后一个WEAK弱引用,虚拟机进行垃圾回收的时候会被清除。可以通过eviction来配置,默认是LRU,flushInterval可以定义刷新间隔(毫秒单位)、size引用数目默认是是1024,readOnly,默认是false,设置为true缓存返回的对象都是一样,且不可修改,可以提升相应性能,false返回的缓存对象的拷贝速度慢,但是更安全。我们也可以实现自定义的缓存只需要实现接口Cache即可。最终会向Configuration的caches数组中添加对应的缓存实例。
- parameterMap不做分析(未来可能别移除)
- 解析结果集映射,就是把表中的属性值与java对象建立对应关系,我们先简单介绍一下resultMap标签属性和子标签。
属性:
id:resultMap的唯一标识
type:resultMap需要转换的java类型
extends:继承父resultMap
autoMapping:自动映射(true|false)
子标签:
constructor:构造方法,也就是你的java类型是有参构造的时候,可以在这里配置对应的构造参数,这个标签下有两个子标签idArg标识作为Id的结果,也就是对应id,arg就是普通的参数对应关系
id:ID值对应
result:就是建立表中与java对象属性值的对应关系,有五个属性值property表示java中的属性,column表中对应列名,javaType java类的全限定名,jdbcType JDBC类型,typeHandler类型转换器(已经分析过)
association :关联,处理一对一数据类型,关系表中一般通过某一个属性值来建立对应关系,所以用户需要设置对应的关联关系字段。有两种管理关系select和嵌套结果集
select关联:设置好属性名、以及数据库中关联的字段名、查询的名称即可,所以不是很难。这里会存在一个N+1问题。
N+1问题: 简单来说就是你主表查询的时候,会同时去查询关联表中的数据,这单独去查询关联表中的就是+1问题。mybatis采用延迟加载方式解决了此类问题,fetchType=lazy即可,也可以设置全局的lazyLoadingEnabled=true。(需要注意的是当调用对象的equals、clone、hashCode、toString方法的时候就会触发延迟加载,可以通过lazyLoadTriggerMethods参数配置哪些方法需要触发多个方法逗号隔开)
关联的嵌套结果映射:如果不想因为延迟加载带来的性能问题,也可以采用关联的嵌套结果映射,思路就是通过数据库查询的连接操作,使用一个sql语句把所需要的信息查询出来,然后在结果映射的时候配置好关联的方式即可(参见官网)。
tip:columnPrefix这个属性的作用是,当嵌套查询有多个一样的数据类型时,可以通过指定不同的别名来区分,比如一篇论文两个作者,那么论文属性中就会有作者A和作者B,两个作者的数据模型是一模一样。那么作者B就可以指定一个别名pf_作者B,那么只需要配置columnPrefix="pf_"即可。
关联的多结果集(ResultSet):有的数据库可以采用存储过程一次性返回多个结果集,也就是执行多个sql语句,可以配置属性resultSets来解析多个结果集,利用resultSet来指定对应的结果集(参见官网)
集合:集合是处理多个关联类型,比如一个作者有多篇文章,可以利用标签< collection/>表示,里边有个ofType属性,表示的是集合中存放的元素值(参见官网)。
当我们使用关联时要注意映射时的性能开销。
鉴别器:< discriminator />可以理解为java中的switch,也就是可以根据不同的值来指定不同的映射,case语句(参见官网)。
自动映射:自动映射就是在没有设置映射的时候,mybatis会自动把查询返回的列名和java对象中查找相同的列名(忽略大小写)对应,我们知道数据库设计的时候,会采用下划线等方式分隔多个单词,而java命名的时候采用驼峰式命名规则,所以可以配置 mapUnderscoreToCamelCase =true,就会自动的映射。有三种自动映射级别NONE:只能手动映射,PARTIAL:对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射,FULL - 自动映射所有属性,默认是PARTIAL。
源码级的数据结构: 首先把每个子标签转换成ResultMapping数据结构,最后在转换成ResultMap数据结构。最后在把ResultMap保存到resultMaps集合中这是一个map集合id是mapper接口全限定名加上resultMap的id值组合。 - 解析sql标签
sql标签是用来定义可以重用的sql代码片段(官网有很好的样例),解析后的标签保存在Configuration的sqlFragments属性中。 - 解析增删改查
private void buildStatementFromContext(List<XNode> list) {
//如果配置了数据库提供商,那么就先匹配带有databaseId的语句
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
//继续匹配不带databaseId的语句如果都存在这个将被舍弃
buildStatementFromContext(list, null);
}
增删改查语句的解析是通过XMLStatementBuilder进行,调用其parseStatementNode()方法。
public void parseStatementNode() {
//获取id值,也就是方法名称
String id = context.getStringAttribute("id");
//获取数据库提供商id
String databaseId = context.getStringAttribute("databaseId");
//判断当前的databaseId是否正确匹配,如果存在,则返回
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}//获取节点名称select/insert/update/delete
String nodeName = context.getNode().getNodeName();
//把当前标签名转换成枚举类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//判断是否是查询语句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//如果没有设置flushCache,则除了select语句外都是true
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//查询语句默认使用缓存
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//缓存主行结果集,当后续使用相同结果集的时候,就直接从缓存中取,默认是false
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
//解析包含include的标签的语句
includeParser.applyIncludes(context.getNode());
//获取参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//获取语言驱动器,默认是XMLLanguageDriver
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
//解析selectKey语句,selectKey主要是为了应对那些不支持自动生成主键id的数据库或驱动,他的功能就是通过sql生成对应的主键id,可以在数据插入之前或之后更新主键id值,这样就达到了这样的效果
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;//实例化一个主键生成器
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//解析具体的sql语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//下边都是获取各种标签属性
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
//保存MappedStatement对象到Configuration对象的mappedStatements属性中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
具体的sql语句是以MappedStatement对象保存的,key值就是mapper接口对应的全限定名和方法名称组合,后续要执行sql语句的时候,首先要获取这个对象。
bindMapperForNamespace():
这个方法是把命名空间转换成接口的class对象,然后调用configuration.addMapper(boundType)方法,把接口对象添加到mapperRegistry属性中,该属性是通过Map<Class<?>, MapperProxyFactory<?>>集合对象保存,也就是最终的接口knownMappers.put(type, new MapperProxyFactory<>(type))是被封装成MapperProxyFactory对象保存。
二、总结
Mybatis会使用XMLMapperBuilder对象来解析映射文件,然后把映射文件中的各种标签通过对应的数据结构保存到Configuration对象中。
- cache-ref:引用缓存保存到cacheRefMap集合
- cache:封装成一个Cache实例,然后保存在Map<String, Cache> caches集合
- resultMap:他的数据结构是ResultMap对象,保存在Map<String, ResultMap> resultMaps集合
- sql:这个是通过标签的id和标签值直接保存在Map<String, XNode> sqlFragments集合中
- select|insert|update|delete:封装成MappedStatement,保存在mapperRegistry属性中
以上就是粗略的对映射文件解析过程进行了分析,到这里整个Configuration对象的解析过程也结束,这对后续的源码分析有很大的帮助。
以上,有任何不对的地方,请指正,敬请谅解。