使用mybatis时我们使用xml来配置我们的sql,这里我们来看一下mybatis是如何将xml解析为可执行的sql的。
mybatis支持的语句并不多只有insert、select、update、delete和被引用的sql五个标签,以及if、choose、when、otherwise、trim、where、set和foreach八个动态语句标签。
其中insert、select、update、delete这四个语句标签只是标示语句的行为,并不会影响到其中的xml解析为sql,在写xml语句时我们也发现在<select>标签中我们也不能省略select。影响生成sql语句的主要还是语句中包含的动态标签。
mybatis分为解析配置和执行sql两个阶段,在使用SqlSessionFactoryBuilder的build方法创建SqlSessionFactory时为配置解析阶段,这期间mybatis会解析xml中的所有的配置和mapper中的所有语句,解析得到的结果存放在Configuration中供执行时使用,mapper中的语句解析便发生在这个时候。
每个insert、select、update、delete语句被解析为MappedStatement存放在Configuration的mappedStatements中:
// src/main/java/org/apache/ibatis/session/Configuration.java
public class Configuration {
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("")
}
key为语句的id,也就是"namespace.语句id"。
MappedStatement
MappedStatement为sql语句的封装,包含了语句中的各个属性和语句的sql。
// src/main/java/org/apache/ibatis/mapping/MappedStatement.java
public final class MappedStatement {
// 语句id
private String id;
// 每次获取结果的量
private Integer fetchSize;
// 语句超时
private Integer timeout;
// 语句的sql
private SqlSource sqlSource;
// 语句类型
private StatementType statementType;
// 结果类型
private ResultSetType resultSetType;
...
}
SqlSource代表可执行sql的‘源’,在执行时通过调用SqlSource的getBoundSql方法来获取当前执行的BoundSql,而BoundSql就代表这一次执行的sql信息,其中包含了可执行sql、sql中引用变量和给到sql的参数。SqlSource是一个接口只定义了getBoundSql这一个方法:
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
parameterObject是我们执行语句时传递的参数。
解析过程
在使用SqlSessionFactoryBuilder的build方法创建SqlSessionFactory时会解析所有的xml配置文件,xml配置由XMLConfigBuilder负责解析,在XMLConfigBuilder会解析配置文件中引用的所有mapper,mapper由XMLMapperBuilder进行解析,XMLMapperBuilder在解析时会对Mapper中的语句逐个进行解析,单个语句的解析由XMLStatementBuilder进行,XMLStatementBuilder解析得到语句的MappedStatement并将其添加到configuration中供执行时使用。
一、sql标签解析
1.1 sql标签使用回顾
sql元素用来定义可被其它语句重复使用的sql内容。sql中可以使用${}定义参数,但与语句中的{}参数为静态参数,需要在<include>中指定并在解析语句时被替换,若<include>中未指定则参数作为语句的一个动态参数在执行语句时去动态替换。
<sql id="userFields">
id, age, ${extraField}
</sql>
<select id="queryUser" resultType="Map">
select
<include refid="userFields" >
<property name="extraField" value="name"/>
</include>
from user where id = ${id}
</select>
include的引用和sql中的参数在加载xml时被解析替换,对于这个例子在解析xml时queryUser对userFields的include就会被替换为:
<select id="queryUser" resultType="Map">
select
id, age, name
from user where id = ${id}
</select>
1.2 sql标签实现
在解析mapper时首先会将mapper中定义的所有sql的dom节点查找出来,然后存放在Configuration的sqlFragments中供后面使用,sql的语句key为'namespace.id'。
// src/main/java/org/apache/ibatis/session/Configuration.java
public class Configuration {
// 存放mapper中的sql节点
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
}
// src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java
// 将sql节点添加到sqlFragments中
public class XMLMapperBuilder extends BaseBuilder {
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 语句唯一id
String id = context.getStringAttribute("id");
id = currentNamespace + "." + id;
// id不存在则添加
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
sqlFragments.put(id, context);
}
}
}
}
在获取到所有的sql节点后,在解析语句时若遇到inclue的节点,则会从Configuration的sqlFragments中获取到include引用的sql的dom节点并复制一个。该操作由XMLIncludeTransformer进行:
private Node findSqlFragment(String refid, Properties variables) {
refid = PropertyParser.parse(refid, variables);
refid = builderAssistant.applyCurrentNamespace(refid, true);
try {
// 获取sql节点
XNode nodeToInclude = configuration.getSqlFragments().get(refid);
// 克隆一个节点
return nodeToInclude.getNode().cloneNode(true);
} catch (IllegalArgumentException e) {
...
}
}
获取到sql的复制节点后,会递归的解析这个节点中的${}的引用,若引用在include中有指定则替换:
// src/main/java/org/apache/ibatis/builder/xml/XMLIncludeTransformer.java
public class XMLIncludeTransformer {
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
...
} else else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// sql节点的子节点中的属性要解析替换${}引用
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
} else if (included
&& (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// sql节点文本节点则直接解析属性并替换
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
}
在解析完参数引用之后要将include节点替换为引用的sql节点,在替换完成后原来的include节点就变为了引用的sql节点,但外面<sql>的这个标签我们并不需要,我们需要的是其里面的内容,所以需要将里面的内容移到外面来,并将sql节点删除:
// src/main/java/org/apache/ibatis/builder/xml/XMLIncludeTransformer.java
public class XMLIncludeTransformer {
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
...
// 若引用的是其它文件中的sql节点,则将其它文件中的sql节点插入到当前dom中
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// include替换为sql
source.getParentNode().replaceChild(toInclude, source);
// 将替换的sql节点的子节点插入到其之前
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
// 其子节点已经全部插入,剩下空sql节点移除
toInclude.getParentNode().removeChild(toInclude);
...
}
}
至此include就全部替换为了指定的sql,并且sql中的#{}引用也被替换,所以sql标签中属性解析发生在语句解析时,我们可以认为它时静态的。
二、SQL树
可以看出我们配置的insert、select、update、delete语句的内容是一个树形结构,其中标签内的文本称为文本子节点而标签自身称为元素节点,在解析时各个节点被解析为SqlNode。除语句的select、update、insert、delete标签,语句里面的每个子标签都有自己的SqlNode,如if的IfSqlNode、trim的TrimSqlNode、文本的TextSqlNode等。
2.1 MixedSqlNode
MixedSqlNode也是SqlNode的一类,它表示一个包含多个子节点的节点。比如insert、select、update、delete这些外层的标签对于语句的解析并没有用,毕竟写了这些标签我们还在要在里面写上insert、select、update、delete,真正需要解析的时其里面的自节点,所以使用MixedSqlNode持有这些子节点。
public class MixedSqlNode implements SqlNode {
// 自节点
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
}
2.2 构建树
xml配置的语句由XMLLanguageDriver进行解析,XMLLanguageDriver的createSqlSource方法会将xml配置解析为SqlSource,SqlSource是初步解析后的语句,在执行时从其中获取可执行的sql。createSqlSource方法中通过XMLScriptBuilder来解析xml,XMLScriptBuilder才是真正干活的地方。
XMLScriptBuilder在解析时对于文本节点封装为TextSqlNode对于包含${}引用的接口封装为StaticTextSqlNode,对于元素节点则使用元素对应的NodeHandler进行解析得到其节点的SqlNode并添加到树中。
开始解析时先获取一级节点,对于是一个元素的一节点则递归的先解析其子节点,最终形成一个SqlNode的树。
public class XMLScriptBuilder extends BaseBuilder {
// 解析节点
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 获取所有一级节点并解析
NodeList children = node.getNode().getChildNodes();
for (int i=0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 文本子节点
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
// 若含有${}则为动态sql
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 元素子节点
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 使用其handler解析解析
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
}
如下面的语句:
<select id="queryUser" resultType="Map">
select * from user where 1=1 and
<if test="id != null">
id = ${id}
</if>
limit 1
</select>
解析后就为:
三、生成sql语句
在完上面的语句树解析后就得到了由各个节点组成的树,其根节点类型为MixedSqlNode包含了其下面的所有一级节点,下面各个节点又包含了自己下面的一级自节点,依次类推,每个节点都是SqlNode的子类型。
SqlNode接口只定义了一个apply接口,用于将自己节点的sql内容添加到最终的sql上。
public interface SqlNode {
boolean apply(DynamicContext context);
}
在获取sql调用根节点的apply方法,根节点再调用其下面包含的一级子节点的apply方法,依次递归的进行,最终各个节点依次自己的情况将自己合适的内容添加到最终的sql上。
public class MixedSqlNode implements SqlNode {
// 所有的一级子节点
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
// 依次调用所有子节点apply
contents.forEach(node -> node.apply(context));
return true;
}
}
比如下面这个if节点:
<if test="id != null">
id = ${id}
</if>
这个if的节点为IfSqlNode,在获取语句时会调用其apply方法,在IfSqlNode的apply中会判断test是否为true若为true则将if中所有内容,这里为‘id = ${id}’追加到语句中。
SqlSource有DynamicSqlSource和StaticSqlSource两种类型的,若一个语句中包含${}的引用或其它元素的子节点则语句封装为DynamicSqlSource否则为StaticSqlSource。
四、节点
生成sql语句关键点还是在于各个节点是如何处理自己节点下的内容,下面列举了几个节点的具体实现。
4.1 StaticTextSqlNode
StaticTextSqlNode是不含${}引用的文本节点。在apply时直接将文本添加到sql中。
// src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java
public class StaticTextSqlNode implements SqlNode {
// 文本内容
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
// 将文本直接添加到sql中
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}
4.2 IfSqlNode
IfSqlNode为if标签的封装,包含test属性中的表达式和if下的子节点,这些子节点保存为MixedSqlNode类型。在apply时通过Ognl解析text中的表达式获并获取表达式的boolean值,若值为true则调用子节点apply将子自节点添加到sql中,若为false则不处理子节点也就是不将子节点添加到sql中。
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
// test内容
private final String test;
// if下的所有一级子节点类型为MixedSqlNode
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// test为true则添加子节点
contents.apply(context);
return true;
}
return false;
}
}
4.3 ChooseSqlNode
ChooseSqlNode为choose的封装,包含了choose下的所有when节点和默认的otherwise节点。其中when的行为与if是相同的,都是test为true则取when中的内容,所以when标签也被封装为IfSqlNode。
ChooseSqlNode在apply时按按照when的顺序来调用when的apply方法,若apply为true则不在调用后面的when节点,当when都为false则调用otherwise节点的apply。
public class ChooseSqlNode implements SqlNode {
//otherwise节点
private final SqlNode defaultSqlNode;
// 所以的when标签,类型为IfSqlNode
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : ifSqlNodes) {
// 为true则不在进行后面的调用
if (sqlNode.apply(context)) {
return true;
}
}
// when都为fals配置了otherwise则调用otherwise的applay
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
}
4.4 ForEachSqlNode
ForEachSqlNode为foreach的封装,ForEachSqlNode中定义了foreach的collection、item、index、open、close和separator属性以及foreach中的子节点:
public class ForEachSqlNode implements SqlNode {
// collection属性
private final String collectionExpression;
// open属性
private final String open;
// close属性
private final String close;
// separator属性
private final String separator;
// item属性
private final String item;
// index属性
private final String index;
// foreach中的节点
private final SqlNode contents;
}
在apply时,先使用ognl解析collection属性并获取表达式的值,若值是Iterable类型的则可以直接迭代访问,若值为数组则为数组创建一个ArrayList并放回,若值为Map则放回Map的entrySet,其它类型或null都将抛出异常。
// src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java
public class ForEachSqlNode implements SqlNode {
public boolean apply(DynamicContext context) {
// 获取collectio的Iterable值
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
}
}
在获取到collection的Iterable的值后,若open不为null则先将open添加到sql语句中:
// src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java
public class ForEachSqlNode implements SqlNode {
private void applyOpen(DynamicContext context) {
if (open != null) {
context.appendSql(open);
}
}
}
之后开始对Iterable进行遍历,对于每个迭代会将#{item}的引用替换为__frch_item_i,i为第几个参数从0开始,#{index}的引用替换为__frch_index_i,依次来标示其唯一引用,并在上下文中添加__frch_item_i和__frch_index_i值为当前迭代的元素和i,最后将foreach下所有节点apply到sql中,如果一个节点非第一个节点则会先添加separator,然后在添加当前迭代的sql。
// src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java
public class ForEachSqlNode implements SqlNode {
public boolean apply(DynamicContext context) {
for (Object o : iterable) {
...
if (first || separator == null) {
// 若时第一个节点或未指定separator则在迭代前添加空字符串
context = new PrefixedContext(context, "");
} else {
// 非第一个节点且指定了separator则在迭代前添加separator
context = new PrefixedContext(context, separator);
}
// 上下文件中添加__frch_item_i和__frch_index_i
if (o instanceof Map.Entry) {
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
// 替换#{item}和#{index}的引用并在sql中追加foreach中的内容
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
}
// 迭代结束添加close
applyClose(context);
}
}