说到动态 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
的核心部分。