您的位置:首页 > 数据库

Mybatis源码解析 —— Sql解析详解

2017-06-04 12:55 513 查看
引言

Sql解析架构
从注解xml 定义到MappedStatement

从MappedStatement到可执行的Sql

结语

引言

Java Server理解与实践 ——Spring集成Mybatis中,笔者简要地介绍了Mybatis框架。

Mybatis框架极大地简化了ORM,让使用者自定义sql语句,再将查询结果映射到Java类中,其中很关键的一部分就是,将用户写的sql语句(以xml或者注解形式)解析成数据库可执行的sql语句。

本文将会介绍这整个的解析过程。

Sql解析架构

本文将以一下的注解定义为例,剖析整个解析过程。

@Select({"<script>",
"SELECT account",
"FROM user",
"WHERE id IN",
"<foreach item='item' index='index' collection='list'",
"open='(' separator=',' close=')'>",
"#{item}",
"</foreach>",
"</script>"})
List<String> selectAccountsByIds(@Param("list") int[] ids);


Sql解析其实分成了两部分,第一部分是将注解/xml中定义的sql语句转化成内存中的MappedStatement,也就是将script部分转化成内存中的MappedStatement,这部分发生在Mybatis初始化的时候,第二部分则是根据MappedStatement以及传入的参数,生成可以直接执行的sql语句,这部分发生在mapper函数被调用的时候。

从注解/xml 定义到MappedStatement

首先,我们要明白什么是MappedStatement,先来看看MappedStatment的类定义。

public final class MappedStatement {

private String resource; //来源,一般为文件名或者是注解的类名
private Configuration configuration; //Mybatis的全局唯一Configuration
private String id; //标志符,可以用于缓存
private Integer fetchSize; //每次需要查询的行数(可选)
private Integer timeout;//超时时间
private StatementType statementType;//语句类型,决定最后将使用Statement, PreparedStatement还是CallableStatement进行查询
private ResultSetType resultSetType;//结果集的读取类型,与java.sql.ResultSet中的类型对应。
private SqlSource sqlSource;//Mybatis中的sqlSource,保存了初次解析的结果
private Cache cache;//缓存空间
private ParameterMap parameterMap;//保存了方法参数与sql语句中的参数对应关系
private List<ResultMap> resultMaps;//可选,定义结果集与Java类型中的字段映射关系
private boolean flushCacheRequired;//是否立即写入
private boolean useCache;//是否使用缓存
private boolean resultOrdered;//可选,默认为false
private SqlCommandType sqlCommandType;//Sql执行类型(增、删、改、查)
private KeyGenerator keyGenerator;//可选,键生成器
private String[] keyProperties;//可选,作为键的属性
c059

private String[] keyColumns;//可选,键的列
private boolean hasNestedResultMaps;//是否有嵌套的映射关系
private String databaseId;//数据库的id
private Log statementLog;//logger
private LanguageDriver lang;//解析器
private String[] resultSets;//可选,数据集的名称
}


可以说,MappedStatement里面包含了Mybatis有关Sql执行的所有特性,在这里我们能够找到Mybatis在Sql执行层面支持哪些特性,比如说可以支持定制化的数据库列到Java属性的映射,定制化的主键生成器。

对MappedStatement有一定的了解之后,我们就可以接着看整个的处理流程了。

处理流程如下图所示:



在这里,我们能够看到清晰的分层结构,从Configuration到具体的NodeHandlers,其实解析的任务一层层分解下来了。另外,最后解析生成的MappedStatement也会注册到Configuration中,保证了Configuration持有对所有MappedStatement的引用。

接下来,我们看一些具体的处理逻辑,也是比较有意思的部分。

XMLLanguageDriver


@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}


XMLLanguageDriver
中,真正发生了解析我们定义在XML/注解中的语句,这里的解析分成了两部分,第一部分,初始化一个Parser,第二部分,对Parser解析出来的节点再进行下一步解析。

而最后生成的SqlSource也分成了两个种,分别为RawSqlSource以及DynamicSqlSource。这样做的好处是,RawSqlSource可以在参数替换完成后直接执行,更加简单高效。

接下来再来看看解析具体Node的方法,
XMLScriptBuilder


List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
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()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
//使用具体的Handler进行处理
NodeHandler handler = nodeHandlers(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return contents;
}


所以,由上可知,最后我们写在Sql定义中的
</foreach>
这样的元素,会由实际的handler(
ForEachHandler
)来进行处理,并且最后也会生成特定的SqlNode用于动态Sql的生成。这样就确保了Mybatis对于元素可扩展性的支持,同时保证了Sql的动态化。

以上,就是从我们的定义,到MappedStatement的整个过程,在这个过程里面,结构化的XML语言被转化成了具体内存里面的一个个SqlNode,而像
ResultMap
这样的注解,则被转化成了MappedStatement里面其它的具体属性。

接下来,我们将会使用MappedStatement,进行可执行的Sql生成。

从MappedStatement到可执行的Sql

从MappedStatement到可执行的Sql部分相对来说就比较简单了,里面只涉及到从RawSqlSource/DynamicSqlSource到StaticSqlSource的转换。

Mybatis中Sql的执行是由
Executor
进行的,而在执行之前,获取了MappedStatement之后,
Executor
会调用MappedStatement的
getBoundSql
方法,具体流程图如下:



而在
DynamicSqlSource
中具体代码如下:

@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
//首先,通过node的apply方法,动态生成占位符,以及相应的参数映射关系
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//然后,再使用SqlSourceBuilder转化成staticSqlSource,这里也会对参数映射关系进行解析
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
//这里的bouldSql中的sql就已经是可执行的sql了
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}


在这里,我们能够看到之前由XMLNode转化而成的SqlNode起作用了,通过其中的Apply方法,会根据参数的长度来添加动态的
?
占位符。

而返回的StaticSqlSource将会交由Executor进行包含参数设定,执行结构映射成Java实例在内的一系列操作,这个将会在后续的文章中继续解析。

结语

本文主要介绍了Mybatis中的Sql解析过程,包含两部分,第一部分是从XML标签/注解到内存中的MappedStatement,第二部分则是从MappedStatement到StaticSqlSource。

这里面,我们能看到Mybatis框架的几个优点:

将Sql语句的动态部分与静态部分分隔开来

在启动时就提前解析XML文件/注解为MappedStatement,提高了效率(不需要执行一次解析一次)

通过标签使得Mybatis的动态Sql具有更加强大的扩展性,也体现了开闭原则。

通过这篇文章,我们能够学习一个Sql生成器的构建思路。也能够更加全面的了解到Mybatis Sql解析所支持的功能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: