Fork me on GitHub

Mybatis原理-动态SQL

自实现

先不谈mybatis是怎么解析SQL的,我们不妨自己先设计一个SQL解析,最基本的功能是,在解析sql的时候,要把接口参数替换到sql里去。

先设定要实现的sql如下,完整的代码见github

1
2
3
4
5
6
7
8
<select id="getInfoById" parameterType="java.lang.Long"
resultType="java.lang.String">
SELECT *FROM ActorPublicNotice
WHERE id=#{id}
and isDeleted=0
and status = 1
limit 1
</select>

为了满足参数替换,我写出如下简单的sql解析方法,传入sql语句和参数列表,用正则匹配出所有#{}的占位符,用参数挨个替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public String parseSql(String sql, Object[] args) {

String parsedSql = sql.replaceAll("\\r?\\n", "");
// 使用正则表达式匹配 #{fieldName} 格式的占位符
//其中{}里的fieldName会被捕获
Pattern pattern = Pattern.compile("#\\{(.*)\\}");
Matcher matcher = pattern.matcher(parsedSql);

int i = 0;
// 逐个替换占位符
while (matcher.find()) {
//整个#{fieldName}
String placeholder = matcher.group();

Object value = args[i++];
// 根据值的类型进行处理
//todo 可增加入参类的映射
String replacement = "";
if (value instanceof String) {
replacement = "'" + value + "'";
} else {
replacement = value.toString();
}
parsedSql = parsedSql.replace(placeholder, replacement);

}

return parsedSql;
}

这个方法但对于一个框架的sql解析来说,用正则匹配的话代码可读性差,且不利于灵活修改。并且如果sql是比较动态的,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
<select id="getInfoById" parameterType="java.lang.Long"
resultType="java.lang.String">
SELECT id FROM ActorPublicNotice
WHERE
<if test="1=1">and isDeleted=1</if>
<if test="2=2">
and actorId=0
<if test="3=3">
and status = 1
</if>
</if>
limit 1
</select>

要怎么解析呢?结合mybatis的功能,我们的解析还需要实现如下功能:

  • 动态节点:动态 SQL 逻辑的节点,如 <if>, <choose>, <where>, <set>, <foreach> 等。比如对于if节点,在拼接sql前会先判断是否满足if节点里的内容。
  • 动态 SQL 逻辑:在这些特殊节点中,可以使用 OGNL(Object-Graph Navigation Language)表达式以及条件判断语句来根据不同情况动态生成 SQL 片段。
  • SQL 拼接顺序:MyBatis 会按照 XML 文件中节点的排列顺序来拼接最终的 SQL 语句,考虑到各个节点之间的逻辑关系。
  • 字段映射:通过配置 ResultMap,MyBatis 可以将数据库查询结果映射到 Java 实体类的属性上,确保查询结果正确地映射到实体对象中。
  • 结果集格式转化:MyBatis 还提供了 TypeHandler 机制,在查询结果映射到 Java 对象时,可以通过自定义 TypeHandler 来实现不同类型之间的转换,从而对结果集进行统一的格式转化。

一起来看看mybatis是怎么做的吧

原理解析

先说结论,mybatis会把每一个xml节点解析成不同的SqlNode,比如就解析成一个IfSqlNode,每一个sqlnode都会有自己的处理方式。全部解析完后,一条sql就是一个树。

mybatis解析动态SQL主要是parseScriptNode()这个方法,这里以If节点的解析为例,附上源码以及简化后的伪代码进行讲解。去掉一些无关的内容改成伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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) {
//简单的追加sql
contents.add(textSqlNode);
}
// 如果是动态节点
else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 获取动态节点的处理器
NodeHandler handler = nodeHandlers(nodeName);
// 用动态节点的处理器去处理,注意这里把父节点传入,用于接收子节点追加sql
handler.handleNode(child, contents);
}
}
return contents;
}
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 递归去遍历子节点,得到节点拼接的sql的content,
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
//获取test属性
String test = nodeToHandle.getStringAttribute("test");
//生成if节点
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
// 把子节点放到新建的if节点里,把if节点放进父节点的content里
targetContents.add(ifSqlNode);
}
}

mybatis-动态SQL-1

首先从\

第二个节点就是动态节点了,动态节点需要调用其重写的handleNode()方法,把处理逻辑交给每个动态节点的apply()去处理。一般在在动态节点的handleNode()方法里,会递归地调用parseDynamicTags(),去依次获取遍历自己的每一个子节点,再把自己放入content。

第三个节点的空的文本节点,把自身文本节点加入content里,文本内容是空。

第四个节点是IF动态节点,在动态节点的handleNode()方法里递归地调用parseDynamicTags(),去依次遍历获取自己的每一个子节点,等递归结束后构造自己IF节点,自身IF动态节点加入content里。

第五个节点是IF动态节点的子IF动态节点,由于它没有子节点了,自身IF动态节点加入content里,结束递归返回。

第六个节点的IF动态节点的子文本节点,把自身文本节点加入content里,文本内容是空。结束本层递归返回上一层。

第七个节点是文本节点,把自身文本节点加入content里,文本内容是”LIMIT 1”。

完整源码走读

附上从mybatis启动开始,怎么执行到解析动态sql的,这部分了解即可。

从最开始讲起,在从mybatis启动开始,会加载xml文件并进行解析。org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement

具体看看下面这行代码对于select|insert|update|delete增删改查节点是怎么解析的

buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

evalNodes()方法已经对xml进行了解析,把该节点内所有的”<…>“都转化成了XNode,返回一个List用于下一步。

接着我们跳入org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext

跳入org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>, java.lang.String)

跳入org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode

parseStatementNode()方法把增删改查节点还有一些特有的属性,比如parameterType、resultMap、resultSetType、useGeneratedKeys,解析出来的。

重点代码是下面这行,返回了一个SqlSource。这里的LanguageDriver默认会使用XMLLanguageDriver创建SqlSource(Configuration构造函数中设置)。

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

跳入org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource

跳入org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode

到这里,我们就可以不跳来跳去了,慢下来仔细看看动态SQL的实际执行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

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();
NodeHandler handler = nodeHandlers(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return contents;
}

private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}

@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}