回顾
前面我们已经看了SQL映射配置文件上的一些核心标签是如何解析的了,同时也认识了二级缓存的构建过程,下面来看一下对于sql我们常常使用的四种标签是如何解析的
现在人然后回到我们的XMLMapperBuilder的configurationElement方法中
上一章,我们对cache、cache-ref、parameterMap、resultMap标签已经分析解析过程了,下面到我们常用的四种sql标签的解析
buildStatementFromContext
对应的方法为XMLMapperBuilder里面的buildStatementFromContext方法
源码如下
可以看到,其会调用重载方法,重载方法的源码如下
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//遍历每个select、update、delete、insert节点
//可以看到,在这里是对其一同处理的
for (XNode context : list) {
//select、update、delete、insert的标签解析是交由XMLStatementBuilder来完成的
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//使用XMLStatementBuilder来解析select、update、delete、insert标签
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//这里同样捕捉InCompleteElementException
//因为存在先后节点先后加载顺序的影响,因此会先记录在configuration中,后续在处理
configuration.addIncompleteStatement(statementParser);
}
}
}
从方法上可以看到,emmmm,对于select、insert、update、delete标签的解析是交由XMLStatementBuilder来进行解析的,MyBatis该不会对于所有标签的解析都是采用建造者模式的吧。。。
不过在此之前,我们先认识一些概念
- 4个节点都会被解析成一个个MappedStatement
- MappedStatement使用SqlSource来存储描述的SQL
下面就先来认识一下SqlSource与MappedStatement
SqlSource
该接口很简单,仅仅只有一个获取绑定的SQL,该方法可以根据注解或配置文件中描述的SQL语句,以及传入的参数,返回可以执行的SQL
MappedStatement
一个MappedStatement就代表一个select、delete、update、insert节点的解析。。。
先认识一下里面的成员属性,不过太多了,我们只讲几个关键的属性
- resources:节点中的id属性,我们常常用这个来对应绑定SQL映射接口文件的方法名,但这个id属性会加上命名空间前缀!
- SqlSource:SqlSource对象,对应绑定的SQL语句
- SqlCommandType:对应的SQL类型,其是一个枚举类,里面有6种类型
- UNKNOWN
- INSERT
- UPDATE
- DELETE
- SELECT
- FLUSH
MapperStatementBuilder的parseStatementNode方法
认识完了这两个类之后,回到我们的解析SQL方法中,我们知道,XMLMapperBuilder将解析sql节点的工作交由了XMLStatementBuilder来进行,并且一个XMLStatementBuilder只解析一个节点!对应的方法为parseStatementNode
该方法源码如下
public void parseStatementNode() {
//获取id属性
String id = context.getStringAttribute("id");
//获取databaseId属性
String databaseId = context.getStringAttribute("databaseId");
//检查databaseId属性与当前configuration的属性是否一致
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//获取节点名字
String nodeName = context.getNode().getNodeName();
//转化为对应的SqlCommandType,学到了,可以使用valueOf方法来根据名字来获取枚举类型实例
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//判断是不是select类型
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//判断需要需要刷新缓存,根据flushCache属性来判断,如果没有flushCache,则根据isSelect属性判断
//可以看到,默认条件下,只要不是查询动作,都会进行刷新缓存!
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//判断需不需要使用缓存,根据属性useCache属性判断,如果没有useCache属性,则根据isSlect属性判断
//可以看到,默认情况下,如果没有配置useCache属性,那么只有select语句会使用缓存
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//判断resultOrdered属性,默认为false
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
//上面已经完成了sql节点里面常用属性的解析
//在解析sql节点前,先解析里面的include标签节点
//解析include标签是交由XMLIncludeTransformer进行的
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
//使用XMLIncludeTransformer去解析include节点
includeParser.applyIncludes(context.getNode());
//解析完include节点之后,继续下面属性的解析
//获取parameterType属性
String parameterType = context.getStringAttribute("parameterType");
//通过别名注册中心去找到parameterType属性对应的类
Class<?> parameterTypeClass = resolveClass(parameterType);
//获取lang属性
String lang = context.getStringAttribute("lang");
//注册LanguageDriver,LanguageDriver用于生成SqlSource的!
LanguageDriver langDriver = getLanguageDriver(lang);
//解析selectKey节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 下面正式开始解析sql,后面再看这整个部分
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;
}
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");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
可以看到总体分为4部
- 根据sql节点的类型和对应的属性去判断是否需要刷新缓存、使用缓存,对应的属性为flushCache和useCache
- 解析include标签
- 解析selectKey标签
- 解析sql
刷新缓存与使用缓存
- 刷新缓存根据flushCache属性来设置,如果没有flushCache属性,默认情况下,不是select类型的都会去刷线缓存
- 使用缓存是根据useCache属性来设置,如果没有useCache属性,默认情况下,只有select类型的才会去使用缓存
很简单。。。
解析include标签
先说明一下include标签的作用
include标签是用来代替对应的sql标签里面的SQL片段的,说白了就是给select等标签引用了sql标签定义的sql片段,并将里面的$ {…}占位符替换成真实的参数,该占位符的作用其实就是引用properties标签里面的配置,所以会去替换成真实的参数,前面我们已经看过了,MyBatis如何解析properties标签的了,对于properties标签是在总配置文件里面的,但会被解析成Properties对象存放进Configuration中,然后在解析Mapper的时候也是可以使用的!!!
下面就来看看是如何解析的
可以看到,XMLStatementBuilder对于include标签的解析,是交由XMLIncludeTransformer来解析的,可以看到其在创建时注入了Configuration和XMLBuilderAssistant
解析的具体方法是applyIncludes
public void applyIncludes(Node source) {
//获取Configuration中的properties,之前在解析总配置文件上
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
//如果configurationVariables不为空就添加进variablesContext中。。
//为什么要这么麻烦呢??
Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
//调用重载的applyIncludes,false代表并不是嵌套进上一层的
//因为此时是第一层,肯定不是嵌套的,后面递归的时候false会变为true,因为发生了嵌套
applyIncludes(source, variablesContext, false);
}
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
//判断标签类型
if ("include".equals(source.getNodeName())) {
//对于include标签的解析,通过refid属性去找到对应的sql片段
//返回的是一个深拷贝的node节点,实际上是一个sql节点
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
//解析include标签下的property节点,该property可以用来注入进sql片段中!
Properties toIncludeContext = getVariablesContext(source, variablesContext);
//递归去处理include节点。。。
//include是可以嵌套的。。sql片段里面嵌入sql片段。。
//因为在sql节点里面也可能使用了include节点。。。
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
//将include节点替换成sql节点
source.getParentNode().replaceChild(toInclude, source);
//判断sql节点里面是不是有子节点
while (toInclude.hasChildNodes()) {
//将sql节点的子节点添加进sql节点前面。。
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
//移除sql节点。。。。
toInclude.getParentNode().removeChild(toInclude);
}
//如果节点类型是一个Element
else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// 获取节点的所有属性
NamedNodeMap attributes = source.getAttributes();
//遍历属性
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
//使用propertyParser去映射去property的值!
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
//对子节点继续处理!
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
}
//如果是嵌套处理,并且节点类型为Text或者SECTION
else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// 使用propertyParser解析节点的值
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
总结一下include标签的解析
- 判断标签的类型,如果是include的话做以下处理
- 首先对property标签进行解析,因为在include标签里面是可以嵌套property标签的
- 通过ref属性找到对应的sql节点,并且深拷贝一份过来
- 对该sql节点继续解析,因为可能存在嵌套
- 递归对后续sql节点的解析会利用propertyParser去进行解析,将${…}占位符替换成property中的实参!
- 最后返回到解析include时,此时对于sql标签已经解析好了
- 使用sql标签替代include标签,然后让sql标签的子标签都插入到sql标签前面,然后最后移除sql标签!
可以看到,对于include标签的解析,其实就是通过递归的方式来解析里面的sql标签,然后使用sql节点代替include节点,再将sql节点中的子节点插入到sql节点前面,最后移除掉sql节点,这里的移除是从select、update等节点对象中移除!
解析include标签还是比较难懂的。。。。。
解析selectKey标签
selectKey标签可以解决主键自增的问题,实现自定义主键自增,在insert、update节点中我们都可以使用selectKey节点来解决主键自增的问题!其实也不算是解决主键自增问题,更像是一种主键生成策略!因为有了自定义的主键生成策略,可以让在完成插入后获取到主键值,里面也是一句SQL!并且通常是一句查询SQL
对应的方法是processSelectKeyNodes方法
源码如下
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
//获取所有的selectKey标签
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
//使用parseSlectKeyNodes方法对selectKey节点进行解析
//需要用到select、update等节点中的parameterType属性和id属性
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
//移除selectKey节点
removeSelectKeyNodes(selectKeyNodes);
}
源码如下
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
//遍历每一个selectKey节点进行处理
for (XNode nodeToHandle : list) {
//使用父标签的id属性和主键生成器的前缀拼接生成id,这里要去拼接前缀
//否则当后续注册selectKey的mappedStatement会与上一层sql的mappedStatement发生冲突
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
//获取selectKey的databaseId属性
String databaseId = nodeToHandle.getStringAttribute("databaseId");
//如果databaseId属性匹配configuration中的id
if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
//对该selectKey节点进行解析
parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
}
}
}
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
//获取seletctresultType属性,代表主键的类型!
String resultType = nodeToHandle.getStringAttribute("resultType");
//根据resultType属性找到对应的类,
Class<?> resultTypeClass = resolveClass(resultType);
//根据statementType属性找到对应的StatementType
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
//获取keyProperty属性,也就是主键值对应的JavaBean的属性名,会将计算出的主键值附上去
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
//获取keyColumn属性,也就是对应数据库的主键名
String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
//首先获取selectKey中的order属性,如果没有默认为after!
//也就是是先执行操作还是先选择主键设置keyProperty
//设置KeyProperty其实就是执行keySelect标签里面的SQL
//如果是before,则会先将插入数据的主键值改为sql的结果,并且keyProperty也会被设置成查出来的结果,后再执行SQL
//如果是after,则会先插入,然后再执行sql查询,此时已经不能改动主键值了,只能设置keyProperty
//一般对于自动增长类型,设为after才会取到正确的值,因此默认为after
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
//默认的一些属性
//不开启缓存
boolean useCache = false;
boolean resultOrdered = false;
//主键值生成策略,默认为NoKeyGenerator
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;
//前面一系列属性处理之后,使用LangDriver创建SqlSource对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
//使用builderAssistant注册MappedStatement
//keySelect也代表一个MappedStatement!!
//其本身就是通过SQL查出或生成主键值!
//如果是before则是查出和生成主键值
//如果是after仅仅只是查出主键值
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
//对id属性拼接上mapper的命名空间
//id是外层insert、select节点的id属性
id = builderAssistant.applyCurrentNamespace(id, false);
//从configuration中获取刚刚创建好的MappedStatement
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
//往configuration中添加KeyGenerator,给该insert节点绑定了对应的SelectKeyGenerator
//KeyGenerator会进行前、后处理,完成前面定义的主键策略
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
其实整个过程分为
- 解析SelectKey中的属性
- keyColumn:对应数据库中的主键名
- keyProperty:对应JavaBean中的属性名
- resultType:主键的数据类型
- order:After、Before,默认是After
- After:仅仅只有查询主键的功能,也就是再完成插入之后,才会执行查询SQL,然后修改keyProperty对应的属性值
- Before:先执行查询SQL,然后使用结果来当作此次插入的主键值,并且修改KeyProperty对应的属性值,再执行插入动作
- 一些默认属性,比如是否使用缓存、是否清空缓存、对应的resultMap、parameterMap,这些属性全部关闭或者设为Null
- 使用LangDriver生成SqlSource,也就是生成、查询主键的sql
- 整合解析出的属性和SqlSource,创建出MappedStatement,然后交由builderAssistant完成注册,添加进Configuration中
- 将对应的SelectKeyGenerator绑定节点的id属性,然后也注册进Configuration中
下面先来看看如何生成SqlSource的
LangDriver
LangDriver是一个接口,用来创建SqlSource和ParameterHandler的
- SqlSource:SQL映射文件或者注解绑定的SQL语句的封装
- ParameterHandler:将Java的参数转化为JDBC类型的
其实现关系如上
XMLLanguageDriver实现了LanguageDriver,然后RawLanaguageDriver继承了XMLLanguageDriver
我们可以在MyBatis的总配置文件上自定义XMLLanguageDriver
默认使用的是XMLLanaguageDriver进行解析
可以看到,对于SqlSource的解析是使用XMLScriptBuilder来进行解析的,Emmmm,对于SqlSouce其实也是使用建造者模式来创建的
源码如下
public SqlSource parseScriptNode() {
//解析sql,判断是不是动态的,通过节点的类型来判断。。。。
//如含有占位符或是含有动态SQL的相关节点,也就是where、set、if那些标签!
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
//判断SQL是不是动态的
if (isDynamic) {
//如果是动态的,则是DynamicSqlSource
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//如果不是动态的,则是RawSqlSource
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
可以看到,XMLLangDriver会返回两种类型的SqlSource,一种是普通的RawSqlSource,一种是动态Sql的DynamicSqlSource
- RawSqlSource:立刻就能使用
- DynamicSqlSource:动态Sql,还需要进一步解析才能使用,判断动态Sql有两个条件
- 有${}占位符的视为动态SQL
- 含有动态SQL相关的节点标签视为动态SQL
- 具体判断的细节在parseDynamicTags中,有兴趣可以去看
nodeHandlerMap里面就存储着一系列动态SQL相关标签的处理器,从中也可以知道哪些标签是根动态SQL相关的
- trim标签
- where标签
- set标签
- foreach标签
- if标签
- choose标签
- when标签
- otherwise标签
- bind标签
NodeHandler
下面来分析一下NodeHandler,从前面可以看到有这么多动态标签的Handler,MyBatis将其都统一抽象成一个NodeHandler,并且是XMLScriptBuilder中的私有内部接口
其实对于上述遇到的各种NodeHandler,其本质上都是去创建对应的SqlNode然后添加进targetContents中
比如WhereHandler
生成对应的whereSqlNode然后添加进targetContents中
其他的也是一致的,就不再赘述
现在selectKey标签也完成解析了
解析SQL
完成include标签和selectKey标签的解析后,下面就到解析Select、insert、update等标签里面的SQL了
返回我们的代码
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 从这里开始,解析sql
//检测有没有对应的KeyGenerator!
//也就是前面我们在解析SelectKey时,会有对应的KeyGenerator生成
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
//如果没有定义,则会根据usseGeneratedKeys属性和SqlCommandType属性来判断
//如果是Insert类型,并且useGeneratedKeys为true会使用Jdbc3KeyGenerator
//如果不是的话,使用NoKeyGenerator(啥都不做)
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 创建对应的sqlSource,已经提到过
// RawSqlSource或者DynamicSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//根据statementType属性来创建出StatementType
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
//获取fetchSize属性
Integer fetchSize = context.getIntAttribute("fetchSize");
//获取timeout属性
Integer timeout = context.getIntAttribute("timeout");
//获取parameterMap属性
String parameterMap = context.getStringAttribute("parameterMap");
//获取resultType属性
String resultType = context.getStringAttribute("resultType");
//根据resultType属性,从别名注册中心找到对应的类
Class<?> resultTypeClass = resolveClass(resultType);
//获取resultMap属性
String resultMap = context.getStringAttribute("resultMap");
//获取resultSetType属性
String resultSetType = context.getStringAttribute("resultSetType");
//
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
//获取keyProperty属性
String keyProperty = context.getStringAttribute("keyProperty");
//获取keyColumn属性
String keyColumn = context.getStringAttribute("keyColumn");
//获取resultSets属性
String resultSets = context.getStringAttribute("resultSets");
//注册MappedStatement
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
步骤如下
-
创建KeyGenerator,会先从Configuration找出前面解析SelectKey标签时创建的KeyGenerator,如果没有,则会根据属性和标签类型来进行选择
- 如果是insert标签,并且useGenerator属性为true,则会使用Jdbc3KeyGenerator,只有后处理,没有前处理
- 如果上面其中一个条件不满足,则是NoKeyGenertor,啥也不做的
-
创建对应的SqlSouce
-
获取对应的一系列属性,比如resultMap,resultType,keyProperty、keyColumn这些。。。
-
MapperBuilderAssistant使用SqlSource、KeyGenerator和一系列属性,创建并且注册MappedStatement(创建MappedStatement实际上是交由MappedBuilderAssistant里面的MappedStatementBuilder来负责的),底层的注册容器也是一个StrictMap。。
-
SQL节点的对应的MappedStatement会与SelectKey的MappedStatement进行区分,selectKey的MappedStatement对应的id是会在sql节点的id属性上拼接上前缀(!selectKey)
至此,对于select、insert、update、delete标签的解析也完成了
总结一下Mapper的解析过程
- XMLMapperBuilder对于select、update、insert、delete标签的解析其实是交由MapperStatementBuilder负责的
- 对于SQL,MyBatis统一抽象成SqlSource
- 对于select、update、insert、delete等,MyBatis统一归类于MappedStatement
- 对于解析4种标签,其实就是创建出对应的MappedStatement出来,然后注册进Configuration中
- 解析Select、update、insert、delete大致分为4个部分
- 解析一些缓存属性
- 解析include标签:通过递归去将include标签对应的sql节点中的${}占位符进行替换,替换完成之后,将解析出来的字符串去替代include标签,此时就完成拼接了。。
- 解析selectKey标签:解析selectKey标签,生成对应的KeyGenerator和MappedStatement,存储进Configuration中
- 解析SQL:此时节点里面已经将include标签和selectKey标签都解析完并且删除了,里面仅仅剩下了SQL语句和一些动态SQL相关的标签,根据剩下的内容去创建出对应的SqlSource;取出前面解析SelectKey创建出的KeyGenerator;最后解析出剩余的属性,根据SqlSource、KeyGenerator和剩余属性,交由MapperBuilderAssistant完成创建MapperStatement并注册
绑定Mapper接口
回到我们的XMLMapperBuilder
现在我们已经完成了configurationElement步骤,解析完了总配置文件下的mapper标签节点,接下来就是bindMapperForNameSpace了,根据命名空间去绑定对应的接口
源码如下
private void bindMapperForNamespace() {
//获取当前命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
//根据命名空间,命名空间其实也是接口的全限定类名
//解析命名空间对应的类
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
//判断是不是已经绑定了
if (boundType != null && !configuration.hasMapper(boundType)) {
//如果还没有绑定
//补充namespace前缀,添加到loadedResource集合中
configuration.addLoadedResource("namespace:" + namespace);
//注册boundType接口,注册进Configuration中的MapperRegistry中
configuration.addMapper(boundType);
}
}
}
绑定Mapper接口,其实就是去注册Mapper接口
- 获取当前命名空间
- 根据命名空间去找到对应的Mapper接口
- 将namespace:命名空间添加进loadedResource中,代表该Mapper已经解析过了
- configuration中注册Mapper接口
处理incomplete*集合
还记得之前我们解析各种标签的时候,有时候会标签之间会存在依赖关系,所以在解析某个标签时会因为依赖的标签还没解析从而失败,会被记录到对应的incomplete*集合中,现在已经完成MyBatis所有配置的解析了,接下来就要去处理这些未完成的集合
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
//处理未完成的resultMaps
parsePendingResultMaps();
//处理未完成的Cache-ref
parsePendingCacheRefs();
//处理未完成的statement
parsePendingStatements();
}
里面其实都是遍历这些集合,然后对各个标签重新解析而已,调用对应的resolve方法
至此,MyBatis的初始化工作就完成了!