需要了解更多可以参考这篇文章https://www.jianshu.com/p/af52bdb8106b
动态SQL
说到动态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的代码如下:
XMLScriptBuilder#parseScriptNode()
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
首先看ifSqlNode的创建:
IfSqlHandler#handleNode()
@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,否则跳过
IfSqlNode#apply()
@Override
public boolean apply(DynamicContext context) {
//通过OGNL判断test的值
if (evaluator.evaluateBoolean(test, context.getBindings())) {
//如果为`true`则遍历子节点执行
contents.apply(context);
return true;
}
//否则跳过
return false;
}
可以看到和前面的推理相符合
ChooseNode
ChooseHandler#handleNode()
@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
ChooseSqlNode#apply()
@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;
}
TrimNode
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的源码:
TrimHandler#handleNode()
@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);
}
这里可以看到,没有其他的处理,只是获取了属性然后初始化
TrimSqlNode#apply()
@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);
}
}
}
ForEachNode
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;
}
BindNode
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的核心部分。