说到动态 SQL
,就不得不提 Script
, Java
作为一个静态语音,代码需要先编译,然后再运行,虽然带来了效率,但是却损失了灵活性。
Spring
为此还专门提供了一套 SpEL
用来封装 Java
脚本语言 API
在 MyBatis
中,也支持动态 SQL
,想要将简单的 String
字符串编译成能运行的代码,需要其他的库的支持, MyBatis
内部使用的是 OGNL
库。
在 OgnlCache
中,是 MyBatis
对 OGNL
的简单封装:
public static Object getValue(String expression, Object root) {
try {
Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
主要便是增加了一层缓存。
有了上面的基础,我们就可以通过需求,来了解实现了:
在 MyBatis
中,动态 SQL
标签有如下几个:
if
:通过条件判断执行 SQL
choose
:通过 switch
选择一条执行 SQL
一般和 when / otherwise
一起使用 trim
: 简单加工 SQL
,比如去除头尾的逗号等,同类的还有 where / set
foreach
: 遍历容器,将遍历的结果拼接成 SQL
bind
: 通过 OGNL
表达式获取指定的值,并绑定到环境变量中 简单的使用方式如下:
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
可以看到,动态 SQL
的关键就是获取 title
的值,然后执行 test
对应的表达式,最后根据结果拼接 SQL
最后也是比较重要的一点就是, MyBatis
的动态 SQL
标签是可以嵌套使用的:
比如:
<update id="update" parameterType="User">
UPDATE users
<trim prefix="SET" prefixOverrides=",">
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age != null and age != ''">
, age = #{age}
</if>
<if test="birthday != null and birthday != ''">
, birthday = #{birthday}
</if>
</trim>
<where> 1=1
<if test="id != null">
and id = ${id}
</if>
</where>
</update>
这样的结构,就像是一颗树,需要层层遍历处理。
前面说到了 MyBatis
处理动态 SQL
的需求,需要处理嵌套的标签。
而这个,恰好符合组合模式的解决场景。
在 MyBatis
中,处理动态 SQL
的关键类如下:
SqlNode
: 用来表示动态标签的相关信息 NodeHandler
: 用来处理 SqlNode
其他信息的类 DynamicContext
: 用来保存处理整个标签过程中,解析出来的信息,主要元素为 StringBuilder
SqlSource
: 用来表示 XML
中 SQL
的信息, MyBatis
中,动态 SQL
最终都会通过 SqlSource
表示
SqlNode
接口的定义如下:
public interface SqlNode {
//处理目前的信息,并将处理完毕的信息追加到DynamicContext 中
boolean apply(DynamicContext context);
}
接下来从 MyBaits
创建以及使用 SqlSource
上来分析动态 SQL
的使用:
创建 SqlSource
的代码如下:
public SqlSource parseScriptNode() {
//创建组合模式中的根节点
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
//如果发现是动态节点,则创建DynamicSqlSource
//反之创建RawSqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
接着看 parseDynamicTags()
protected MixedSqlNode parseDynamicTags(XNode node) {
//使用list保存所有sqlNode
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));
//如果节点是Text节点,则使用TextSqlNode处理
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) {
String nodeName = child.getNode().getNodeName();
//通过节点名获取节点的处理类
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//处理节点
handler.handleNode(child, contents);
isDynamic = true;
}
}
//返回根节点
return new MixedSqlNode(contents);
}
TextSqlNode
作用之一便是检测 SQL
中是否包含 ${}
,如果包含,则判断为 Dynamic
。
TextSqlNode
的作用主要和 #{xxx}
类似,但是实现方式不同, #{xxx}
底层是通过 JDBC#ParperedStatement
的 setXXX
方法设置参数,具有防止 SQL
注入的功能,而 TextSqlNode
则是直接替换的 String
,不会做任何的 SQL
处理,因此一般不建议使用。
接下来再看 MixedSqlNode
,它的作用是作为根节点:
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;
}
}
可以看见,非常简单,就是用来遍历所有子节点,分别调用 apply()
方法。
接下来我们看看其他标签的使用:
首先看 ifSqlNode
的创建:
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//加载子节点信息
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//获取Test表达式信息
String test = nodeToHandle.getStringAttribute("test");
//将信息传入`ifSqlNode`
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
可以看到,这里 IfNode
也充当了一个根节点,里面包含了其子节点信息。
这里可以大概猜想处理, IfSqlNode
会通过 OGNL
执行 test
的内容,如果 true
,则执行后面的 SqlNode
,否则跳过
@Override
public boolean apply(DynamicContext context) {
//通过OGNL判断test的值
if (evaluator.evaluateBoolean(test, context.getBindings())) {
//如果为`true`则遍历子节点执行
contents.apply(context);
return true;
}
//否则跳过
return false;
}
可以看到和前面的推理相符合
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> whenSqlNodes = new ArrayList<>();
List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
//遍历子节点,生成对应的SqlNode 将其保存在各个对应的容器中
//whenSqlNode 的处理和IfNode的处理相同
handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
//验证otherwise的数量的合法性,只能有一个otherwise节点
SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
//生成对应的ChooseSqlNode
ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
targetContents.add(chooseSqlNode);
}
这里就可以猜想到 ChooseNode
对 Node
的处理的,应该是遍历所有的 ifNode
,然后当遇到符合条件的,边处理后续的 Node
,否则执行 otherwise
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
TrimeNode
是对 SQL
语句进行加工。
其包含3个属性:
prefix
: 需要添加的前缀 suffix
: 需要添加的尾缀 prefixOverrides
: 当 SQL
是以此标志开头的时候,需要移除的开头的内容 suffixOverrides
: 当 SQL
是以此标志结尾的时候,需要移除的结尾的内容 现在举个例子:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
</trim>
</select>
可以看到, trim
会自动为 SQL
增加 Where
前缀,同时当 state
为 null
的时候, SQL
会以 AND
开头,此时 trim
标签便会自动将 AND
删除。
同理, SET
可能会遇到 ,
结尾,只需要使用 suffixOverrides
删除结尾即可,这里不再叙述。
接下来查看 Trim
的源码:
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//获取子节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//获取前缀
String prefix = nodeToHandle.getStringAttribute("prefix");
//获取前缀需要删除的内容
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
//获取尾缀
String suffix = nodeToHandle.getStringAttribute("suffix");
//获取尾缀需要删除的内容
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
//创建`TrimSqlNode`
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
这里可以看到,没有其他的处理,只是获取了属性然后初始化
@Override
public boolean apply(DynamicContext context) {
//创建FilteredDynamicContext对象
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
//获取子元素的处理结果
boolean result = contents.apply(filteredDynamicContext);
//整体拼接SQL
filteredDynamicContext.applyAll();
return result;
}
这里出现了一个新的对象: FilteredDynamicContext
, FilteredDynamicContext
继承自 DynamicContext
,其相对于 DynamicContext
仅仅多了一个新的方法: applyAll()
,
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
//添加前缀
applyPrefix(sqlBuffer, trimmedUppercaseSql);
//添加后缀
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
其中, applyPrefix()
方法会检查 SQL
是否 startWith()
需要删除的元素,如果有,则删除。
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
//如果SQL以toRemove开头,则删除
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
foreach
节点的元素很多:
item
: 遍历的时候所获取的元素的具体的值,类似 for(String item:list )
中的 item
,对于 Map
, item
对应为 value
index
: 遍历的时候所遍历的索引,类似 for(int i=0;i<10;i++)
中的 i
,对于 Map
, index
对应为 key
collection
: 需要遍历的集合的参数名字,如果指定了 @Param
,则名字为 @Param
指定的名字,否则如果只有一个参数,且这个参数是集合的话,需要使用 MyBatis
包装的名字:
Collection
: 名字为 collection
List
: 名字为 list
array
相关代码如下:
private Object wrapCollection(final Object object) {
if (object instanceof Collection) {
StrictMap<Object> map = new StrictMap<>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
StrictMap<Object> map = new StrictMap<>();
map.put("array", object);
return map;
}
return object;
}
open
: 类似 TrimNode
中的 prefix
close
: 类似 TrimNode
中的 suffix
separator
: 每个 SQL
的分割符
使用方式如下:
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
以上元素没有默认值,当没有设置的时候, MyBatis
便不会设置相关的值,对于 open
或 close
,我们一般都会自己加上括号,所以有时候可以不设置。
接下来我们查看 MyBatis
的 foreach
的源码:
ForEachNode
的初始化代码没什么好看的,就是简单的获取相关的属性,然后初始化。我们直接看其 apply()
方法。
public boolean apply(DynamicContext context) {
//准备添加绑定
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
//追加Open符号
applyOpen(context);
//记录索引,用来赋值给`index`
int i = 0;
//调用`OGNL`的迭代器
for (Object o : iterable) {
//PrefixedContext继承自DynamicContext,主要是增加了分隔符
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
//对于Map key会绑定到index , value会绑定到item上
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
//实时绑定i到index上
applyIndex(context, i, uniqueNumber);
//实时绑定具体的值到item上
applyItem(context, o, uniqueNumber);
}
//生成对应的占位符,并绑定相关的值#{__frch_item_1}等
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
//追加结尾符
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
bind
节点可以方便的运行 OGNL
表达式,并将结果绑定到指定的变量。
使用方法如下:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
一般可以内置使用的元素为 _parameter
表示现在的参数,以及 _databaseId
,表示现在的 database id
对于 BindNode
,对应的是 VarDeclSqlNode
,具体的代码这里不再细看,大概就是使用 OGNL
获取具体的值,比较简单。
对于动态 SQL
的节点对应的类,我们就分析完了,可以看到 SqlNode
完美的应用了组合模式,每个 SqlNode
都保存了其子节点下面的节点,执行下来便像是一颗树的递归。
当然, SqlNode
的使用仅仅是动态 SQL
的一部分,但是它确实动态 SQL
的核心部分。