前文主要分析了Spring中XML配置文件方式的加载MappedStatement,本文在前文的基础上简单了解下Mybatis的SQL语法并且看下CRUD节点是如何被解析的.
mapper的动态sql语法
1.if,条件判断
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
2.choose/when/otherwise,类似于if/else语句判断
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
3.trim,多搭配where进行SQL语句拼装
<!--prefix/suffix属性均是由prefixOverrides/suffixOverrides的条件满足后实行的覆盖策略-->
<!--对开头有AND或者OR的值直接替换为WHERE-->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
4.set,类比数据库的set关键字(其会消除多余的,分隔符)
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
5.foreach,集合迭代输出
<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>
6.bind,创建额外属性并应用
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
7._paramter和_databaseId是mybatis的内部属性。前者代表方法参数类,多与bind节点搭配使用,后者则为settings属性指定的数据库标识id
SQL节点解析帮助类-XMLLanguageDriver
Mybatis默认XML驱动类为XMLLanguageDriver,其主要作用于解析select|update|insert|delete节点为完整的SQL语句。
笔者此处直接查看主要方法createSqlSource()代码
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
引出XMLScriptBuilder#parseScriptNode()方法,我们再继续观察如何创建SqlSource对象(真实目的也是为了解析SQL节点)
public SqlSource parseScriptNode() {
//获取select/update/insert/delete节点下的Sql语句节点包装成相应的SqlNode对象
//每个CRUD语句可能都有多个SqlNode对象
List<SqlNode> contents = parseDynamicTags(context);
//包装成混合型SqlNode对象,'Mixed'译为混合,很贴切的名字
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
//是否为动态语句
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//否则生成普通版SqlSource
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
笔者此处简单看下对各类节点的归类处理,XMLScriptBuilder#parseDynamicTags()方法
private List<SqlNode> parseDynamicTags(XNode node) {
//获取CRUD节点下所有子节点,包括文本内容<trim>等动态sql节点
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) {
//直接获取数据内容,类似"select * from .."纯文本语句
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//TextSqlNode对象主要回去检查纯文本语句是否含有'${'和'}'字符串,有则为true
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
//返回最普通的含有data的StaticTextSqlNode对象
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
//获取节点名对应的NodeHandler对象,无非就是TrimNodeHandler/WhereNodeHandler等等
NodeHandler handler = nodeHandlers.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//调用同一接口实现解析生成SqlNode对象并统一存入List<SqlNode>集合中
handler.handleNode(child, contents);
isDynamic = true;
}
}
return contents;
}
SqlNode是一个接口,内部只有一个方法apply(),通过parseDynamicTags()方法获取的SqlNode集合是为了方便SqlSource类去真正的生成SQL语句
SqlNode
优先观察下公共接口类SqlNode,代码如下
public interface SqlNode {
boolean apply(DynamicContext context);
}
内部只有apply()方法,其应该会在上下文context操刀,我们可以看下有哪些类型的SqlNode
TextSqlNode
对类型为CDATA块或者TEXT的包装为TextSqlNode对象,形如
<select id="test">
<![CDATA[
select * from tb_test
]]>
</select>
或者
<select id="test">
select * from tb_test where name = ${name}
</select>
笔者此处看下其是如何实现apply()方法把,代码如下
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context));
context.appendSql(parser.parse(text));
return true;
}
关于上述代码中的GenericTokenParser是如何解析sql的,笔者就不展开了,大意上是对含有${}字符的值进行相应的绑定替换,从而生成完整的sql保存至DynamicContext对象中
另外此类还有一个关键的方法isDynamic(),其是为了判断相应的字符串是否为动态SQL
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
// same as
GenericTokenParser parser = createParser(checker);
parser.parse(text);
// true while the text contains '${}'
return checker.isDynamic();
}
只有SQL语句含有${}标志符号,才会返回true
StaticTextSqlNode
保存无特殊字符${}的SQL语句,即其是建立在上述的TextSqlNode#isDynamic()方法,在返回为false的情况下被包装。其apply()方法也特别的简单
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
MixedSqlNode
其内部只有一个类型为java.util.List的contents属性,主要保存多种类型的SqlNode,相当于暂时当个仓库而已
public class MixedSqlNode implements SqlNode {
private List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
其余类型的SqlNode,笔者放在下述的板块结合NodeHandler来一起讲解
NodeHandler
主要应用于对trim/where/set/if等类型为ELEMENT节点的补充解析。
其是org.apache.ibatis.scripting.xmltags.XMLScriptBuilder的内部类
private Map<String, NodeHandler> nodeHandlers = new HashMap<String, NodeHandler>() {
private static final long serialVersionUID = 7123056019193266281L;
{
put("trim", new TrimHandler());
put("where", new WhereHandler());
put("set", new SetHandler());
put("foreach", new ForEachHandler());
put("if", new IfHandler());
put("choose", new ChooseHandler());
put("when", new IfHandler());
put("otherwise", new OtherwiseHandler());
put("bind", new BindHandler());
}
};
针对上述代码笔者作下简单的分析
BindHandler.
用于解析bind标签节点
private class BindHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//获取name和value属性
final String name = nodeToHandle.getStringAttribute("name");
final String expression = nodeToHandle.getStringAttribute("value");
//包装成简单的VarDeclSqlNode类
final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
targetContents.add(node);
}
}
VarDeclSqlNode
public class VarDeclSqlNode implements SqlNode {
private final String name;
private final String expression;
public VarDeclSqlNode(String var, String exp) {
name = var;
expression = exp;
}
public boolean apply(DynamicContext context) {
final Object value = OgnlCache.getValue(expression, context.getBindings());
context.bind(name, value);
return true;
}
}
很明显就是将name和计算后的真实value值对应关系保存至DynamicContext#bindings(HashMap类型)中
TrimHandler
用于解析trim标签节点
private class TrimHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//trim标签下可包含where/set/if/when等标签,将之封装成MixedSqlNode
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// read prefix/preffixOverrides/suffix/suffixOverrides properties
String prefix = nodeToHandle.getStringAttribute("prefix");
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
String suffix = nodeToHandle.getStringAttribute("suffix");
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
// delegate TrimSqlNode to process trim sql
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
TrimSqlNode
首先观察下构造函数
public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}
protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
//这里contents一般为MixedSqlNode,内部包含多个SqlNode
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}
读取的preffixOverrides/suffixOverrides属性可以是符合以|为分隔符的字符串,比如"and | or"会被包装为List["AND","OR"]形式
笔者此处再简单看下apply()方法,代码如下
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
// first,parse nested nodes
boolean result = contents.apply(filteredDynamicContext);
// aim to prefixOverrides and suffixOverrides,generate corrent sql
filteredDynamicContext.applyAll();
return result;
}
经过上述的代码执行后,生成的sql语句会被转化为大写形式
WhereHandler
用于解析where标签节点
private class WhereHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// same as TrimSqlNode
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}
WhereSqlNode
经过查看,其是TrimSqlNode的子类,简单看下其源码
public class WhereSqlNode extends TrimSqlNode {
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
很简单,就是固定了参数prefix=WHERE和prefixOverrides=List["AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"],其余调用父类方法即可,详见上文
SetHandler
用于解析set标签节点
private class SetHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// same as TrimSqlNode
SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
targetContents.add(set);
}
}
SetSqlNode
经过查看,其也是TrimSqlNode的子类,简单看下其源码
public class SetSqlNode extends TrimSqlNode {
private static List<String> suffixList = Arrays.asList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", null, null, suffixList);
}
}
很简单,就是固定了参数prefix=SET、suffix=null(会将suffixOverrides符合的条件置为空)、suffixOverrides=List[","],其余调用父类方法即可,详见上文
ForEachHandler
用于解析foreach标签节点
private class ForEachHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// read collection/item/index/open/close/separator properties
String collection = nodeToHandle.getStringAttribute("collection");
String item = nodeToHandle.getStringAttribute("item");
String index = nodeToHandle.getStringAttribute("index");
String open = nodeToHandle.getStringAttribute("open");
String close = nodeToHandle.getStringAttribute("close");
String separator = nodeToHandle.getStringAttribute("separator");
// independent SqlNode
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
foreach节点的属性简析如下
collection 代表的是集合的类型,例如list代表参数类型为List.class,array代表参数类型为数组类型
item 代表集合的value值
index 代表集合的key值,可为下标值也可为HashMap中的key值
open 类似于prefix属性
close 类似于suffix属性
separator 拼装的分隔符号,多为','
ForEachSqlNode
独立的类,限于比较复杂,笔者此处只查看下其构造函数,有兴趣的读者可自行分析
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
//解析帮助类
this.evaluator = new ExpressionEvaluator();
// 集合别名
this.collectionExpression = collectionExpression;
this.contents = contents;
this.open = open;
this.close = close;
this.separator = separator;
this.index = index;
this.item = item;
this.configuration = configuration;
}
IfHandler
用于解析if标签节点
private class IfHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
// read test properties
String test = nodeToHandle.getStringAttribute("test");
//
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
IfSqlNode
条件判断解析类,内部代码很简单,如下
public class IfSqlNode implements SqlNode {
private ExpressionEvaluator evaluator;
private String test;
private SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
public boolean apply(DynamicContext context) {
//主要作用即是用于条件的判断
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
org.apache.ibatis.scripting.xmltags.ExpressionEvaluator主要是应用OGNL语法进行解析类似name !=null,其会读取上下文中是否有对应的属性值。具体的读者可自行分析
OtherwiseHandler/ChooseHandler
用于解析otherwise/choose/when节点,这三者一般搭配使用
1.OtherwiseHandler
private class OtherwiseHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
targetContents.add(mixedSqlNode);
}
}
2.ChooseHandler,内含choose/when的解析
private class ChooseHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> whenSqlNodes = new ArrayList<SqlNode>();
List<SqlNode> otherwiseSqlNodes = new ArrayList<SqlNode>();
//解析choose...when..otherwise结构
handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
//检查otherwise标签是否只有一个,大于一个则报错
SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
targetContents.add(chooseSqlNode);
}
// when标签使用IfHandler解析,otherwise标签使用OtherwiseHandler
private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) {
List<XNode> children = chooseSqlNode.getChildren();
for (XNode child : children) {
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlers.get(nodeName);
if (handler instanceof IfHandler) {
handler.handleNode(child, ifSqlNodes);
} else if (handler instanceof OtherwiseHandler) {
handler.handleNode(child, defaultSqlNodes);
}
}
}
private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
SqlNode defaultSqlNode = null;
if (defaultSqlNodes.size() == 1) {
defaultSqlNode = defaultSqlNodes.get(0);
} else if (defaultSqlNodes.size() > 1) {
throw new BuilderException("Too many default (otherwise) elements in choose statement.");
}
return defaultSqlNode;
}
}
ChooseSqlNode
public class ChooseSqlNode implements SqlNode {
private SqlNode defaultSqlNode;
private List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
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;
}
}
ChooseSqlNode存放多个IfSqlNode和单个TextSqlNode/StaticTextSqlNode
choose..when..otherwise结构类似于java的switch..case结构
总结
where、set标签均可看做是trim标签的子类
choose与when/otherwise搭配使用,可以有多个when标签,只允许至多单个otherwise标签
除了bind标签,其余标签底下均可以有多个其他标签
TextSqlNode/StaticTextSqlNode可以说是CRUD解析sql的基础类
网友评论