mybatis之动态sql语句解析

ゝ一纸荒年。 2022-09-05 01:42 588阅读 0赞

写在前面

本文在这篇文章基础上进行分析,详细解析sql语句相关的解析工作。
想要系统学习的,可以参考这篇文章,重要!!!。

入口

在初始化解析全局配置文件的过程中,会执行到如下的方法:

  1. org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
  2. public void parse() {
  3. if (!configuration.isResourceLoaded(resource)) {
  4. // 获取mapper xml中的<mapper/>标签,并执行解析
  5. // <2021-08-16 14:26:06>
  6. configurationElement(parser.evalNode("/mapper"));
  7. configuration.addLoadedResource(resource);
  8. bindMapperForNamespace();
  9. }
  10. ...snip...
  11. }

<2021-08-16 14:26:06>处是获取mapper xml中的<mapper/>标签,并执行解析,源码如下:

  1. org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
  2. private void configurationElement(XNode context) {
  3. try {
  4. // 获取当前<mapper/>标签的命名空间
  5. String namespace = context.getStringAttribute("namespace");
  6. if (namespace.equals("")) {
  7. throw new BuilderException("Mapper's namespace cannot be empty");
  8. }
  9. builderAssistant.setCurrentNamespace(namespace);
  10. // 解析<cache-ref/>标签
  11. cacheRefElement(context.evalNode("cache-ref"));
  12. // 解析<cache/>标签
  13. cacheElement(context.evalNode("cache"));
  14. // 解析<parameterMap>标签
  15. parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  16. // 解析<resultMap/>标签
  17. resultMapElements(context.evalNodes("/mapper/resultMap"));
  18. // 解析<sql/>标签
  19. sqlElement(context.evalNodes("/mapper/sql"));
  20. // 解析增删改查标签
  21. // <2021-08-16 14:31:13>
  22. buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  23. } catch (Exception e) {
  24. throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  25. }
  26. }

<2021-08-16 14:31:13>处是解析增删改查标签,源码如下:

  1. org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>)
  2. private void buildStatementFromContext(List<XNode> list) {
  3. if (configuration.getDatabaseId() != null) {
  4. buildStatementFromContext(list, configuration.getDatabaseId());
  5. }
  6. // <2021-08-16 14:32:09>
  7. buildStatementFromContext(list, null);
  8. }

<2021-08-16 14:32:09>处源码如下:

  1. org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>, java.lang.String)
  2. private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  3. // 遍历所有的增删改查节点,依次处理
  4. for(XNode context : list) {
  5. final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
  6. try {
  7. // 执行解析,生成org.apache.ibatis.mapping.MappedStatement
  8. // <2021-08-16 14:34:24>
  9. statementParser.parseStatementNode();
  10. } catch (IncompleteElementException e) {
  11. configuration.addIncompleteStatement(statementParser);
  12. }
  13. }
  14. }

<2021-08-16 14:34:24>处是解析增删改查节点并生成对应的org.apache.ibatis.mapping.MappedStatement对象,源码如下:

  1. org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
  2. public void parseStatementNode() {
  3. // 解析增删改查节点的id属性
  4. String id = context.getStringAttribute("id");
  5. // 解析增删改查节点的databaseId属性
  6. String databaseId = context.getStringAttribute("databaseId");
  7. if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;
  8. // 解析增删改查节点的fetchSize属性
  9. Integer fetchSize = context.getIntAttribute("fetchSize");
  10. // 解析增删改查节点的timeout属性
  11. Integer timeout = context.getIntAttribute("timeout");
  12. // 解析增删改查节点的parameterMap属性
  13. String parameterMap = context.getStringAttribute("parameterMap");
  14. // 解析增删改查节点的parameterType属性
  15. String parameterType = context.getStringAttribute("parameterType");
  16. Class<?> parameterTypeClass = resolveClass(parameterType);
  17. // 解析增删改节点的resultMap属性
  18. String resultMap = context.getStringAttribute("resultMap");
  19. // 解析增删改查节点的resultType属性
  20. String resultType = context.getStringAttribute("resultType");
  21. // 解析增删改查节点的lang属性
  22. String lang = context.getStringAttribute("lang");
  23. // 创建lang属性对应的org.apache.ibatis.scripting.LanguageDriver实例
  24. LanguageDriver langDriver = getLanguageDriver(lang);
  25. Class<?> resultTypeClass = resolveClass(resultType);
  26. String resultSetType = context.getStringAttribute("resultSetType");
  27. // 获取statementType属性值对应的org.apache.ibatis.mapping.StatementType枚举值
  28. StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  29. ...snip...
  30. // 解析sql语句,并生成org.apache.ibatis.scripting.SqlSource对象
  31. // <2021-08-16 14:43:47>
  32. SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  33. ...snip...
  34. // 创建org.apache.ibatis.mapping.MappedStatement对象并添加到org.apache.ibatis.session.Configuration
  35. // 全局配置文件对象中
  36. builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
  37. fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
  38. resultSetTypeEnum, flushCache, useCache, resultOrdered,
  39. keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  40. }

<2021-08-16 14:43:47>就是我们要分析的程序的入口了,下面我们就从这里开始吧,具体参考1:解析sql语句

1:解析sql语句

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, org.apache.ibatis.parsing.XNode, java.lang.Class<?>)
  2. public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  3. // 创建XMLScriptBuilder对象
  4. XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  5. // 解析script节点
  6. // <2021-08-16 15:14:39>
  7. return builder.parseScriptNode();
  8. }

<2021-08-16 15:14:39>处是解析script动态sql语句节点,源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
  2. public SqlSource parseScriptNode() {
  3. // 获取所有的节点sql节点
  4. // <2021-08-16 15:28:30>
  5. List<SqlNode> contents = parseDynamicTags(context);
  6. MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
  7. SqlSource sqlSource = null;
  8. if (isDynamic) {
  9. sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  10. } else {
  11. sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  12. }
  13. return sqlSource;
  14. }

<2021-08-16 15:28:30>处是获取增删改查标签内部的所有的节点,具体参考1.1:获取增删改查标签内节点

1.1:获取增删改查标签内节点

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
  2. private List<SqlNode> parseDynamicTags(XNode node) {
  3. // 结果集合
  4. List<SqlNode> contents = new ArrayList<SqlNode>();
  5. // 获取增删改查节点内的所有的子节点
  6. NodeList children = node.getNode().getChildNodes();
  7. // 遍历所有的子节点依次处理
  8. for (int i = 0; i < children.getLength(); i++) {
  9. // 封装当前节点为org.apache.ibatis.parsing.XNode对象
  10. XNode child = node.newXNode(children.item(i));
  11. // 如果是节点是CDATA(输入包含特殊字符文本使用),或者是普通的文本节点
  12. if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
  13. // 获取文本的内容,如"\n SELECT * FROM test_dynamic_sql \n"
  14. // 这里的\n是换行符
  15. String data = child.getStringBody("");
  16. // 使用文本创建org.apache.ibatis.parsing.xmltags.TextSqlNode对象
  17. TextSqlNode textSqlNode = new TextSqlNode(data);
  18. // 如果是动态sql语句则直接添加,并标记动态标记为true,一般以下两种情况为动态sql
  19. // 1:在sql语句中包含${xxx} 2:包含动态sql语句节点,如<if/>,<where/>,<trim/>,<foreach/>等
  20. // 此处因为处理的是CDATA和文本的节点情况,所以是情况1
  21. // <2021-08-16 15:43:23>
  22. if (textSqlNode.isDynamic()) {
  23. contents.add(textSqlNode);
  24. isDynamic = true;
  25. // 否则创建StaticTextSqlNode并添加到结果集合中
  26. } else {
  27. contents.add(new StaticTextSqlNode(data));
  28. }
  29. // 如果是元素节点,一般是<if/>,<foreach/>,<where/>,<trim/>等标签时,这里为true
  30. } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
  31. // 获取节点的名称,如if,foreach,where,trim等
  32. String nodeName = child.getNode().getNodeName();
  33. // 根据当前动态sql语句节点的名称获取对应的节点处理器
  34. // <2021-08-16 17:11:57>
  35. NodeHandler handler = nodeHandlers.get(nodeName);
  36. // 如果是没有获取到对应的处理器类,说明节点信息错误,直接异常
  37. if (handler == null) {
  38. throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
  39. }
  40. // 调用具体的处理器类处理动态sql语句节点
  41. // <2021-08-16 17:30:33>
  42. handler.handleNode(child, contents);
  43. // 将动态sql语句的标记设置为true
  44. isDynamic = true;
  45. }
  46. }
  47. return contents;
  48. }

<2021-08-16 15:43:23>处是判断是否为动态sql语句,具体参考1.1.1:判断是否为动态sql<2021-08-16 17:11:57>处是根据动态sql语句节点的名称获取对应的处理器,具体参考1.1.2:根据动态节点名称获取处理器<2021-08-16 17:30:33>处是使用动态sql标签的处理器来进行处理,具体参考2:各种动态sql标签处理器

1.1.1:判断是否为动态sql

源码如下:

  1. org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic
  2. public boolean isDynamic() {
  3. // 创建DynamicCheckerTokenParser对象
  4. DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
  5. // 创建解析器
  6. // <2021-08-16 15:47:51>
  7. GenericTokenParser parser = createParser(checker);
  8. // 使用解析器解析文本
  9. parser.parse(text);
  10. // 返回是否为动态sql语句
  11. return checker.isDynamic();
  12. }

<2021-08-16 15:47:51>处是创建用于判断是否包含指定标记的解析器,源码如下:

  1. org.apache.ibatis.scripting.xmltags.TextSqlNode#createParser
  2. private GenericTokenParser createParser(TokenHandler handler) {
  3. // 创建用于判断是否包含${xxx}的解析器,即如果是在sql文本中包含${xxx}则就是动态sql语句
  4. return new GenericTokenParser("${", "}", handler);
  5. }

1.1.2:根据动态节点名称获取处理器

源码中的noteHanders是一个Map和初始化如下:

  1. private Map<String, NodeHandler> nodeHandlers = new HashMap<String, NodeHandler>() {
  2. private static final long serialVersionUID = 7123056019193266281L;
  3. {
  4. put("trim", new TrimHandler());
  5. put("where", new WhereHandler());
  6. put("set", new SetHandler());
  7. put("foreach", new ForEachHandler());
  8. put("if", new IfHandler());
  9. put("choose", new ChooseHandler());
  10. put("when", new IfHandler());
  11. put("otherwise", new OtherwiseHandler());
  12. put("bind", new BindHandler());
  13. }
  14. };

处理类的接口定义如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.NodeHandler
  2. private interface NodeHandler {
  3. void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  4. }

2:各种动态sql标签处理器

对应的接口如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.NodeHandler
  2. private interface NodeHandler {
  3. void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  4. }

主要的实现类如下:
在这里插入图片描述
接下来我们分别看下每个实现类。

2.1:BindHandler

可能的配置,如:

  1. <select id="queryListWithXmlDynamicSqlBind" resultMap="myResultMap" statementType="PREPARED">
  2. <!-- 添加%模糊查询符号,这样程序里就需要添加了,不然程序调用几次就要添加几次,而这里只需要一次 -->
  3. <bind name="myname_add_percent" value="'%' + myname + '%'"/>
  4. SELECT * FROM test_dynamic_sql t WHERE t.`myname` LIKE #{myname_add_percent}
  5. </select>

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.BindHandler
  2. private class BindHandler implements NodeHandler {
  3. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  4. // 获取name属性的值
  5. final String name = nodeToHandle.getStringAttribute("name");
  6. // 获取绑定后的value值
  7. final String expression = nodeToHandle.getStringAttribute("value");
  8. // <2021-08-17 10:37:18>
  9. final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
  10. targetContents.add(node);
  11. }
  12. }

<2021-08-17 10:37:18>处VarDeclSqlNode类继承自接口SqlNode,接口源码如下:

  1. org.apache.ibatis.scripting.xmltags.SqlNode
  2. // 每个xml的接口都会解析会解析成该类型的一个实例,只不过不同节点对应到接口的不同子类
  3. // 如<bind/>节点对应到的就是BindHandle类
  4. public interface SqlNode {
  5. boolean apply(DynamicContext context);
  6. }

VarDeclSqlNode源码如下:

  1. org.apache.ibatis.scripting.xmltags.VarDeclSqlNode
  2. public class VarDeclSqlNode implements SqlNode {
  3. // 存储bind标签中name属性的值
  4. private final String name;
  5. // 存储bind标签中value属性的值,因为可能是ognl表达式,所以这里变量名称为expression
  6. private final String expression;
  7. // 构造函数
  8. public VarDeclSqlNode(String var, String exp) {
  9. name = var;
  10. expression = exp;
  11. }
  12. public boolean apply(DynamicContext context) {
  13. // 从ognl表达式缓存中获取值,该类是一个负责解析和缓存ognl表达式的类
  14. final Object value = OgnlCache.getValue(expression, context.getBindings());
  15. // 绑定到上下文中,其实就是存储bind标签对应的键值对到map中,方便后续获取使用
  16. context.bind(name, value);
  17. // 某些节点对应的SqlNode子类需要用到该布尔返回值,这里不需要,直接返回true
  18. return true;
  19. }
  20. }

2.2:TrimHandler

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.TrimHandler
  2. private class TrimHandler implements NodeHandler {
  3. publicvoid handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  4. // 获取statement的所有的sql节点信息
  5. // <2021-08-18 13:17:19>
  6. List<SqlNode> contents = parseDynamicTags(nodeToHandle);
  7. // 创建MixedSqlNode对象
  8. MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
  9. // 获取用于动态添加的前缀pefix属性值
  10. String prefix = nodeToHandle.getStringAttribute("prefix");
  11. // 获取用于动态删除的前缀prefixOverrides值
  12. String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
  13. // 获取用于动态添加的后缀suffix的值
  14. String suffix = nodeToHandle.getStringAttribute("suffix");
  15. // 获取用于动态删除的后缀suffixOverrides的值
  16. String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
  17. // 创建<trim/>节点对应的节点对象TrimSqlNode
  18. // <2021-08-18 13:35:48>
  19. TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
  20. targetContents.add(trim);
  21. }
  22. }

<2021-08-18 13:17:19>处是获取所有的sql节点,比如如下可能的配置和对应的结果:

  1. <trim prefix="set" suffixOverrides=",">
  2. <if test="myname!=null">myname=#{myname},</if>
  3. <if test="myage!=null">myage=#{myage}</if>
  4. </trim>

在这里插入图片描述
元素分别如下:

  1. 0trim后的换行和空格内容
  2. 1:第一个if标签的内容
  3. 2:第一个if标签后的换行和空格内容
  4. 3:第二个if的内容
  5. 4:第二个if后的换行和空格内容

<2021-08-18 13:35:48>处是创建trim节点对应的TrimSqlNode对象,关于TrimSqlNode参考3.1:TrimSqlNode

2.3:WhereHandler

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.WhereHandler
  2. private class WhereHandler implements NodeHandler {
  3. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  4. // 获取所有的子节点
  5. List<SqlNode> contents = parseDynamicTags(nodeToHandle);
  6. // 使用<where/>标签子节点封装为MixedSqlNode对象
  7. MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
  8. // 创建WhereSqlNode对象
  9. // <2021-08-18 15:03:25>
  10. WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
  11. targetContents.add(where);
  12. }
  13. }

<2021-08-18 15:03:25>处是创建WhereSqlNode,关于WhereSqlNode具体参考3.2:WhereSqlNode

2.4:SetHandler

对应的是动态sql语句中的<set>标签,用在更新语句中,源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.SetHandler
  2. private class SetHandler implements NodeHandler {
  3. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  4. // 获取<set/>标签的所有子节点
  5. List<SqlNode> contents = parseDynamicTags(nodeToHandle);
  6. // 通过<set/>标签的子节点创建MixedSqlNode节点
  7. MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
  8. // 创建SetSqlNode节点
  9. // <2021-08-18 15:15:07>
  10. SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
  11. targetContents.add(set);
  12. }
  13. }

<2021-08-18 15:15:07>处是通过<set>标签子节点创建SetSqlNode对象,关于SetSqlNode具体参考3.3:SetSqlNode

2.5:ForEachHandler

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.ForEachHandler
  2. private class ForEachHandler implements NodeHandler {
  3. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  4. // 获取<foreach/>标签的所有子节点(包含换行符等各种不可见字符)
  5. List<SqlNode> contents = parseDynamicTags(nodeToHandle);
  6. // 使用<foreach/>标签的子节点创建org.apache.ibatis.scripting.xmltags.MixedSqlNode对象
  7. MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
  8. // 获取collection属性,该属性用于配置集合的类型
  9. String collection = nodeToHandle.getStringAttribute("collection");
  10. // 获取item属性值,该值用于设置遍历集合的每个条目的变量名
  11. String item = nodeToHandle.getStringAttribute("item");
  12. // 索引值
  13. String index = nodeToHandle.getStringAttribute("index");
  14. // 获取open属性值,即需要添加在最前面的信息,如in,为"("
  15. String open = nodeToHandle.getStringAttribute("open");
  16. // 获取close属性值,即需要添加在最后面的信息,如in,为")"
  17. String close = nodeToHandle.getStringAttribute("close");
  18. // 获取separator属性值,即遍历生成的每个元素的分割符,如in,为","
  19. String separator = nodeToHandle.getStringAttribute("separator");
  20. // 创建<foreach>标签对应的ForEachSqlNode节点
  21. // <2021-08-18 16:31:11>
  22. ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
  23. targetContents.add(forEachSqlNode);
  24. }
  25. }

<2021-08-18 16:31:11>处是通过<foreach>标签的配置信息创建ForEachNode对象,具体参考3.4:ForEachNode

2.6:IfHandler

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.IfHandler
  2. private class IfHandler implements NodeHandler {
  3. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  4. // 获取<if>节点的子节点
  5. List<SqlNode> contents = parseDynamicTags(nodeToHandle);
  6. // 将<if>节点子节点封装为MixedSqlNode节点
  7. MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
  8. // 获取<if>标签中的test属性的值
  9. String test = nodeToHandle.getStringAttribute("test");
  10. // 将<if>标签封装为对应的IfSqlNode对象
  11. // <2021-08-18 17:39:56>
  12. IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
  13. targetContents.add(ifSqlNode);
  14. }
  15. }

<2021-08-18 17:39:56>是将<if>标签封装为对应的IfSqlNode对象,具体参考3.5:IfSqlNode

2.7:OtherwiseHandler

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.OtherwiseHandler
  2. private class OtherwiseHandler implements NodeHandler {
  3. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  4. // 获取<otherwise>节点的所有子节点
  5. List<SqlNode> contents = parseDynamicTags(nodeToHandle);
  6. // 将<otherwise>节点的子节点封装为MixedSqlNode对象
  7. MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
  8. // 直接添加
  9. targetContents.add(mixedSqlNode);
  10. }
  11. }

2.8:ChooseHandler

源码如下:

  1. org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.ChooseHandler
  2. private class ChooseHandler implements NodeHandler {
  3. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  4. // <choose/>标签的<when/>子标签集合
  5. List<SqlNode> whenSqlNodes = new ArrayList<SqlNode>();
  6. // <choose/>标签的<otherwise/>子标签集合
  7. List<SqlNode> otherwiseSqlNodes = new ArrayList<SqlNode>();
  8. // 处理<when>,<otherwise>标签
  9. handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
  10. // 获取默认标签,即<otherwise>标签
  11. SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
  12. // 将<when>标签集合和<otherwise>标签集合封装为ChooseSqlNode对象
  13. // <2021-08-18 18:01:38>
  14. ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
  15. targetContents.add(chooseSqlNode);
  16. }
  17. private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) {
  18. List<XNode> children = chooseSqlNode.getChildren();
  19. for (XNode child : children) {
  20. // 根据节点名称获取对应的节点处理器
  21. String nodeName = child.getNode().getNodeName();
  22. NodeHandler handler = nodeHandlers.get(nodeName);
  23. // 这里需要注意<when>标签对应的处理器和<if>标签是一样的,因为都是判断逻辑,标签名称不同而已
  24. // <2021-08-18 18:07:10>
  25. if (handler instanceof IfHandler) {
  26. handler.handleNode(child, ifSqlNodes);
  27. // otherwise标签情况,对应的handler是OtherwiseHandler
  28. // <2021-08-18 18:08:19>
  29. } else if (handler instanceof OtherwiseHandler) {
  30. handler.handleNode(child, defaultSqlNodes);
  31. }
  32. }
  33. }
  34. // 获取唯一的<otherwise>节点,<otherwise>节点要么没有要么只能有一个
  35. private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
  36. SqlNode defaultSqlNode = null;
  37. // 需要注意的一点是<otherwise>节点只能有一个,因此会有一个对于长度>1是抛出异常的逻辑
  38. if (defaultSqlNodes.size() == 1) {
  39. defaultSqlNode = defaultSqlNodes.get(0);
  40. } else if (defaultSqlNodes.size() > 1) {
  41. throw new BuilderException("Too many default (otherwise) elements in choose statement.");
  42. }
  43. return defaultSqlNode;
  44. }
  45. }

<2021-08-18 18:01:38>处是将when节点信息和otherwise节点信息封装为ChooseSqlNode,关于ChooseSqlNode参考3.6:ChooseSqlNode<2021-08-18 18:07:10>处的IfHandler具体参考2.6:IfHandler<2021-08-18 18:08:19>处是处理otherwise标签,对应的handle是OtherwiseHandler,具体参考2.7:OtherwiseHandler

3:各种标签对应的节点

对应的接口如下:

  1. org.apache.ibatis.scripting.xmltags.SqlNode
  2. public interface SqlNode {
  3. boolean apply(DynamicContext context);
  4. }

3.1:TrimSqlNode

这是<trim/>节点对应的类,源码如下:
构造函数,源码如下:

  1. private SqlNode contents;
  2. private String prefix;
  3. private String suffix;
  4. private List<String> prefixesToOverride;
  5. private List<String> suffixesToOverride;
  6. private Configuration configuration;
  7. public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
  8. this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
  9. }
  10. protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
  11. // 所有的子节点集合构成的MixSqlNode对象
  12. this.contents = contents;
  13. // trim标签中配置的prefix属性值
  14. this.prefix = prefix;
  15. // trim标签中配置的prefixesOverride值
  16. this.prefixesToOverride = prefixesToOverride;
  17. // trim标签中配置的suffix值
  18. this.suffix = suffix;
  19. // trim标签中配置的suffixesToOverrides值
  20. this.suffixesToOverride = suffixesToOverride;
  21. // 全局配置文件对应的org.apache.ibatis.session.Configuration对象
  22. this.configuration = configuration;
  23. }

apply方法源码如下:

  1. org.apache.ibatis.scripting.xmltags.TrimSqlNode#apply
  2. public boolean apply(DynamicContext context) {
  3. // 创建FiteredDynamicContext对象,该对象用于辅助动态sql语句生成最终sql
  4. // <2021-08-18 13:50:50>
  5. FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
  6. // 这里的contents是封装了trim子节点信息的MixedSqlNode对象
  7. // <2021-08-18 14:16:06>
  8. boolean result = contents.apply(filteredDynamicContext);
  9. // 应用trim信息,生成最终的sql语句
  10. // <2021-08-18 14:35:05>
  11. filteredDynamicContext.applyAll();
  12. return result;
  13. }

<2021-08-18 13:50:50>处FiteredDynamicContext对象参考2.1.1:FiteredDynamicContext<2021-08-18 14:16:06>处是应用trim的子节点信息,具体参考2.1.2:应用trim子节点信息<2021-08-18 14:35:05>处是应用trim的配置信息,生成最终sql,具体参考2.1.3:应用trim信息生成最终sql

3.1.1:FiteredDynamicContext

构造函数源码如下:

  1. org.apache.ibatis.scripting.xmltags.TrimSqlNode.FilteredDynamicContext#FilteredDynamicContext
  2. public FilteredDynamicContext(DynamicContext delegate) {
  3. super(configuration, null);
  4. // 设置被代理的对象,该对象是封装了动态sql语句和参数相关信息
  5. this.delegate = delegate;
  6. // 是否已经应用了前缀的标记,默认为false
  7. this.prefixApplied = false;
  8. // 是否已经引用了后缀的标记,默认为false
  9. this.suffixApplied = false;
  10. // 生成的sql语句信息
  11. this.sqlBuffer = new StringBuilder();
  12. }

3.1.2:应用trim子节点信息

源码如下:

  1. org.apache.ibatis.scripting.xmltags.MixedSqlNode#apply
  2. public boolean apply(DynamicContext context) {
  3. // 遍历trim节点的所有子节点信息,依次调用自己的apply方法
  4. for (SqlNode sqlNode : contents) {
  5. // 调用当前SqlNode的apply方法
  6. // <2021-08-18 14:20:59>
  7. sqlNode.apply(context);
  8. }
  9. return true;
  10. }

<2021-08-18 14:20:59>处当节点为StaticTextSqlNode时参考2.1.2.1:StaticTextSqlNode的apply
<2021-08-18 14:20:59>处当节点为IfSqlNode时参考2.1.2.2:IfSqlNode的apply。通过这个过程来拼接生成多个<if>标签构成的sql语句,如下配置:

  1. UPDATE test_dynamic_sql
  2. <trim prefix="set" suffixOverrides=",">
  3. <if test="myname!=null">myname=#{myname},</if>
  4. <if test="myage!=null">myage=#{myage}</if>
  5. </trim>

当第一个条件满足时,最终的sql语句是UPDATE test_dynamic_sql myname=#{myname},,注意此时sql语句少一个set,多一个,这个添加set和去除,的工作需要通过后续的代码完成,,具体参考2.1.3:应用trim信息生成最终sql

3.1.2.1:StaticTextSqlNode的apply

源码如下:

  1. org.apache.ibatis.scripting.xmltags.StaticTextSqlNode#apply
  2. public boolean apply(DynamicContext context) {
  3. // 拼接sql
  4. // <2021-08-18 14:23:07>
  5. context.appendSql(text);
  6. return true;
  7. }

<2021-08-18 14:23:07>处源码如下:

  1. org.apache.ibatis.scripting.xmltags.TrimSqlNode.FilteredDynamicContext#appendSql
  2. public void appendSql(String sql) {
  3. // 直接拼接
  4. // private StringBuilder sqlBuffer;
  5. sqlBuffer.append(sql);
  6. }
3.1.2.2:IfSqlNode的apply

源码如下:

  1. org.apache.ibatis.scripting.xmltags.IfSqlNode#apply
  2. public boolean apply(DynamicContext context) {
  3. // 判断是否符合条件,符合条件才继续
  4. if (evaluator.evaluateBoolean(test, context.getBindings())) {
  5. // private SqlNode contents;
  6. // 此时contents又是一个MiexSqlNode,相当于是递归调用
  7. contents.apply(context);
  8. return true;
  9. }
  10. return false;
  11. }

3.1.3:应用trim信息生成最终sql

源码如下:

  1. org.apache.ibatis.scripting.xmltags.TrimSqlNode.FilteredDynamicContext#applyAll
  2. public void applyAll() {
  3. // 获取当前的sql语句,如myname=#{myname},
  4. sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
  5. // 转小写,避免大小写造成问题
  6. String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
  7. if (trimmedUppercaseSql.length() > 0) {
  8. // 应用前缀 如update语句添加set
  9. // <2021-08-18 14:39:29>
  10. applyPrefix(sqlBuffer, trimmedUppercaseSql);
  11. applySuffix(sqlBuffer, trimmedUppercaseSql);
  12. }
  13. delegate.appendSql(sqlBuffer.toString());
  14. }

<2021-08-18 14:39:29>处源码如下:

  1. org.apache.ibatis.scripting.xmltags.TrimSqlNode.FilteredDynamicContext#applyPrefix
  2. private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  3. // 还没有应用前缀才继续
  4. if (!prefixApplied) {
  5. // 设置前缀已经应用,方式重复添加前缀
  6. prefixApplied = true;
  7. // 如果是需要动态删除的前缀不为空,则先动态删除
  8. // 比如配置<trim prefix="where" suffixOverrides="and">,此时sql为
  9. // “and name='张三' and age='90'”, 处理后就是 ‘ name='张三' and age='90'”
  10. if (prefixesToOverride != null) {
  11. for (String toRemove : prefixesToOverride) {
  12. if (trimmedUppercaseSql.startsWith(toRemove)) {
  13. sql.delete(0, toRemove.trim().length());
  14. break;
  15. }
  16. }
  17. }
  18. // 如果是prefix为不空,则动态添加
  19. // 比如配置<trim prefix="where" suffixOverrides="and">,此时sql为
  20. // “name='张三' and age='90'”, 处理后就是 ‘where name='张三' and age='90'”
  21. if (prefix != null) {
  22. // 先添加个空格,防止sql语法错误
  23. sql.insert(0, " ");
  24. sql.insert(0, prefix);
  25. }
  26. }
  27. }

3.2:WhereSqlNode

构造函数如下:

  1. org.apache.ibatis.scripting.xmltags.WhereSqlNode
  2. // 注意这是TrimSqlNode的一个子类,因为where标签只是trim标签的一种特殊情况而已,所以
  3. // 直接继承然后指定prefix和prefixOverrides就可以了,不需要实现其他逻辑
  4. public class WhereSqlNode extends TrimSqlNode {
  5. // where中可能的需要替换的前缀,列举了我们写程序时所有可能的写法
  6. private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
  7. public WhereSqlNode(Configuration configuration, SqlNode contents) {
  8. // 直接调用父类TrimSqlNode创建对象
  9. // <2021-08-18 15:08:11>
  10. super(configuration, contents, "WHERE", prefixList, null, null);
  11. }
  12. }

<2021-08-18 15:08:11>处是通过通过父类org.apache.ibatis.scripting.xmltags.TrimSqlNode创建实例,关于org.apache.ibatis.scripting.xmltags.TrimSqlNode具体参考3.1:TrimSqlNode

3.3:SetSqlNode

源码如下:

  1. org.apache.ibatis.scripting.xmltags.SetSqlNode
  2. // 这里继承org.apache.ibatis.scripting.xmltags.TrimSqlNode的原因是
  3. // <set>标签时<trim>标签的一种特殊情况而已,即相当于是trim的如下设置
  4. // <trim prefix="set" suffixOverrides=",">
  5. public class SetSqlNode extends TrimSqlNode {
  6. // suffixOverrides的集合值
  7. private static List<String> suffixList = Arrays.asList(",");
  8. public SetSqlNode(Configuration configuration,SqlNode contents) {
  9. // 将prefix这是为set,并调用父类构造函数创建实例
  10. // <2021-08-18 15:20:28>
  11. super(configuration, contents, "SET", null, null, suffixList);
  12. }
  13. }

<2021-08-18 15:20:28>处是设置设置前缀为set,并调用父类org.apache.ibatis.scripting.xmltags,TrimSqlNode创建实例,关于org.apache.ibatis.scirpting.xmltags.TrimSqlNode具体参考3.1:TrimSqlNode

3.4:ForEachNode

构造函数如下:

  1. org.apache.ibatis.scripting.xmltags.ForEachSqlNode#ForEachSqlNode
  2. public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
  3. // 表达式解析器
  4. this.evaluator = new ExpressionEvaluator();
  5. // 集合的表达式,即在foreach标签中的collection属性中设置的值,如list
  6. this.collectionExpression = collectionExpression;
  7. // 封装foreach标签子节点的MixedSqlNode对象
  8. this.contents = contents;
  9. // foreach标签中open属性设置的值
  10. this.open = open;
  11. // foreach标签中close属性设置的值
  12. this.close = close;
  13. // foreach标签中separator设置的值
  14. this.separator = separator;
  15. // foreach标签中index属性设置的值
  16. this.index = index;
  17. // foreach标签中item属性设置的值
  18. this.item = item;
  19. // 全局配置文件对应的org.apache.ibatis.session.Configuration对象
  20. this.configuration = configuration;
  21. }

apply源码如下:

  1. org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply
  2. public boolean apply(DynamicContext context) {
  3. // 获取参数信息
  4. // <2021-08-18 16:41:50>
  5. Map<String, Object> bindings = context.getBindings();
  6. // 获取指定集合的迭代器对象,后续使用该迭代器对象迭代每个元素
  7. final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
  8. // 一个元素都没有的情况
  9. if (!iterable.iterator().hasNext()) {
  10. return true;
  11. }
  12. boolean first = true;
  13. // 先添加open属性的值
  14. // <2021-08-18 16:46:50>
  15. applyOpen(context);
  16. int i = 0;
  17. for (Object o : iterable) {
  18. DynamicContext oldContext = context;
  19. // 如果当前是第一个元素
  20. if (first) {
  21. // 创建PrefixedContext实例,构造函数如下:
  22. /* private DynamicContext delegate; private String prefix; private boolean prefixApplied; public PrefixedContext(DynamicContext delegate, String prefix) { super(configuration, null); this.delegate = delegate; // 前缀 this.prefix = prefix; // 是否已经应用前缀 this.prefixApplied = false; } */
  23. // 该对象用于处理分隔符,因为首个元素不需要分隔符,所以这里传入""
  24. context = new PrefixedContext(context, "");
  25. // 非首个元素情况
  26. } else {
  27. // 如果是<foreach>标签中设置了separator属性值,则传入分割符,否则就是需要分隔符,直接传入""
  28. if (separator != null) {
  29. context = new PrefixedContext(context, separator);
  30. } else {
  31. context = new PrefixedContext(context, "");
  32. }
  33. }
  34. // 生成唯一数字,用于存储索引值,和当前遍历的条目对象
  35. int uniqueNumber = context.getUniqueNumber();
  36. // 如果是map.entry
  37. if (o instanceof Map.Entry) { // Issue #709
  38. @SuppressWarnings("unchecked")
  39. Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
  40. // 使用map.entry的键作为索引值,以uniqueNumber为标识存储
  41. applyIndex(context, mapEntry.getKey(), uniqueNumber);
  42. // 使用map.entry的value作为条目值,以uniqueNumber为标识存储
  43. applyItem(context, mapEntry.getValue(), uniqueNumber);
  44. // list,set等的情况
  45. } else {
  46. // 使用i,即当前遍历的位置作为索引值,使用uniqueNumber作为唯一标识存储
  47. applyIndex(context, i, uniqueNumber);
  48. // 使用o,即当前遍历的条目作为条目值,使用uniqueNumber作为唯一标识存储
  49. applyItem(context, o, uniqueNumber);
  50. }
  51. // 调用MixedSqlNode应用<foreach>标签的所有子节点,其实就是生成sql语句
  52. // <2021-08-18 17:22:05>
  53. contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
  54. // 判断prefix是否已经插入
  55. if (first) first = !((PrefixedContext) context).isPrefixApplied();
  56. context = oldContext;
  57. i++;
  58. }
  59. // 添加close后缀
  60. applyClose(context);
  61. return true;
  62. }

<2021-08-18 16:41:50>处是获取foreach的collection属性值对应的集合的参数信息,如下可能的结果:
在这里插入图片描述
<2021-08-18 16:46:50>处是添加open属性的值,源码如下:

  1. org.apache.ibatis.scripting.xmltags.ForEachSqlNode#applyOpen
  2. private void applyOpen(DynamicContext context) {
  3. // 添加sql语句
  4. if (open != null) {
  5. // 该处代码再前面已经分析过了,其实就是向StringBuilder中追加信息,这里是添加open属性值
  6. context.appendSql(open);
  7. }
  8. }

<2021-08-18 17:22:05>是应用foreach标签的子节点生成sql语句,具体参考3.1.2:应用trim子节点信息

3.5:IfSqlNode

构造函数如下:

  1. private ExpressionEvaluator evaluator;
  2. private String test;
  3. private SqlNode contents;
  4. public IfSqlNode(SqlNode contents, String test) {
  5. this.test = test;
  6. this.contents = contents;
  7. this.evaluator = new ExpressionEvaluator();
  8. }

apply方法源码如下:

  1. org.apache.ibatis.scripting.xmltags.IfSqlNode#apply
  2. public boolean apply(DynamicContext context) {
  3. // 如果是test表达式的值为true
  4. if (evaluator.evaluateBoolean(test, context.getBindings())) {
  5. // 应用<if>标签的所有子节点,生成sql语句
  6. contents.apply(context);
  7. return true;
  8. }
  9. return false;
  10. }

3.6:ChooseSqlNode

源码如下:

  1. public class ChooseSqlNode implements SqlNode {
  2. private SqlNode defaultSqlNode;
  3. private List<SqlNode> ifSqlNodes;
  4. public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
  5. this.ifSqlNodes = ifSqlNodes;
  6. this.defaultSqlNode = defaultSqlNode;
  7. }
  8. public boolean apply(DynamicContext context) {
  9. for (SqlNode sqlNode : ifSqlNodes) {
  10. if (sqlNode.apply(context)) {
  11. return true;
  12. }
  13. }
  14. if (defaultSqlNode != null) {
  15. defaultSqlNode.apply(context);
  16. return true;
  17. }
  18. return false;
  19. }
  20. }

写在后面

本部分通过分析scripting包相关的API,看了针对动态sql语句是如何进行解析的,这些解析逻辑是通过org.apache.ibatis.mapping.SqlSource的API调用获取sql语句时来执行的,关于SqlSource的具体内容可以参考这篇文章。

发表评论

表情:
评论列表 (有 0 条评论,588人围观)

还没有评论,来说两句吧...

相关阅读

    相关 MyBatis动态SQL语句

    ​前言我们在进行项目开发时,经常遇到需要根据不同的需求,对原有SQL语句的内容进行修改,原来这是一个比较头疼的问题,因为需要对原有SQL语句进行拼接、重组,费时费力还容易出错

    相关 MyBatis动态SQL语句

    在Mapper配置文件中,有时候需要根据一些查询条件来选择不同的SQL语句,或者将一些使用频率高的SQL语句单独配置,在需要的地方引用。MyBatis提供了一种可以根据条件动态