likes
comments
collection
share

MyBatis映射器文件解析:动态SQL语句大家好,我是王有志。今天我们来学习 MyBatis 应用程序解析动态 SQL

作者站长头像
站长
· 阅读数 31

XMLScriptBuilder#parseDynamicTags 方法分析

protected MixedSqlNode parseDynamicTags(XNode node) {
  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));
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      // 处理静态 SQL 语句,省略
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
      // 处理动态 SQL 语句
      String nodeName = child.getNode().getNodeName();  
      NodeHandler handler = nodeHandlerMap.get(nodeName);   
      handler.handleNode(child, contents);  
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
  }

首先我们来明确下 XMLScriptBuilder#parseDynamicTags 方法的入参,XNode 实例是我们在 MyBatis 映射器中配置的每一条完整的 SQL 语句,如下:MyBatis映射器文件解析:动态SQL语句大家好,我是王有志。今天我们来学习 MyBatis 应用程序解析动态 SQL第 2 行代码,创建了 List 容器 contents,用于存储生成的 SqlNode 实例。第 3 行代码,用于获取该 SQL 语句中的所有子元素,注意这里的子元素不仅仅是图中所示的 where 元素等,还包括图中展示的文本元素,如:select * from order_item 。那么 NodeList 中会有多少子元素呢?如上图所示的 SQL 语句中子元素会有 3 个:select * from order_item ,where 元素以及一个回车符,那么我们可以知道 Node#getChildNodes 方法并不会递归获取子元素的子元素,另外后面我们会忽略这个回车符。接下来进入第 4 行的循环语句,我们看针对 NodeList 中的元素的处理:

下面我们着重分析第 11 行代码和第 12 行代码中出现的对象和方法进行分析。

XMLScriptBuilder#initNodeHandlerMap 方法

private void initNodeHandlerMap() {
  nodeHandlerMap.put("trim", new TrimHandler());
  nodeHandlerMap.put("where", new WhereHandler());
  nodeHandlerMap.put("set", new SetHandler());
  nodeHandlerMap.put("foreach", new ForEachHandler());
  nodeHandlerMap.put("if", new IfHandler());
  nodeHandlerMap.put("choose", new ChooseHandler());
  nodeHandlerMap.put("when", new IfHandler());
  nodeHandlerMap.put("otherwise", new OtherwiseHandler());
  nodeHandlerMap.put("bind", new BindHandler());
}

源码非常简单,只是向容器 nodeHandlerMap 中添加 NodeHandler 实例,其中 Key 是 MyBatis 映射器中提供的动态元素的名称,而 Value 则是不同动态元素的 NodeHandler 实现类的实例。这里注意,MyBatis 提供的用于实现动态 SQL 语句的元素有 9 个,而对应的 NodeHandler 实现类总共有 8 个,这是因为 if 元素与 where 元素共用了实现类 IfHandler。

NodeHandler 的结构

NodeHandler 是 XMLScriptBuilder 定义的内部接口,并且 NodeHandler 的实现类也都是 XMLScriptBuilder 的内部类。我理解 MyBatis 将 NodeHandler 和 NodeHandler 的实现类定义为 XMLScriptBuilder 的内部接口和内部类是因为它们的功能不会扩散出 XMLScriptBuilder,因此只需要定义在 XMLScriptBuilder 内部即可。XMLScriptBuilder 有 8 个实现类,如下:MyBatis映射器文件解析:动态SQL语句大家好,我是王有志。今天我们来学习 MyBatis 应用程序解析动态 SQLNodeHandler 接口中只定义了一个方法:

private interface NodeHandler {  
  void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);  
}

而 NodeHandler 的实现类在实现 NodeHandler#handleNode 方法时,没有进行特别的处理,只是解析对应元素中的属性,并生成相应的 SqlNode,这里我以最常见的 IfHandler 为例进行说明,源码如下:

private class IfHandler implements NodeHandler {
  public IfHandler() {
  }

  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    String test = nodeToHandle.getStringAttribute("test");
    IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
    targetContents.add(ifSqlNode);
  }
}

可以看到在 IfHandler#handleNode 方法中,首先调用了 XMLScriptBuilder#parseDynamicTags 方法,这是因为大部分动态 SQL 语句的元素中都支持嵌套子元素,而 Node#getChildNodes 方法无法获取再次嵌套的子元素,因此在支持嵌套子元素的动态 SQL 语句元素中,要先调用 XMLScriptBuilder#parseDynamicTags 方法处理嵌套的子元素,这里结合 XMLScriptBuilder#parseDynamicTags 方法的调用逻辑,可以看到这里也是一个递归调用。Tips:在所有 NodeHandler 的实现类中,只有 BindHandler 和 ChooseHandler 中没有调用 XMLScriptBuilder#parseDynamicTags 方法。第 8 行代码,获取了 if 元素的 test 属性,并在第 9 行代码中构建了 IfSqlNode 实例。第 10 行代码,将 IfSqlNode 实例保存到 XMLScriptBuilder#parseDynamicTags 方法中创建的变量 contents 中。第 9 行代码,创建了 if 元素对应的 SqlNode 实例 IfSqlNode,构造方法很简单,这里就不和大家一起看了。第 10 行代码,将创建的 IfSqlNode 实例添加到 targetContents 中,这里的 targetContents 就是在 XMLScriptBuilder#parseDynamicTags 方法中创建的 contents。

SqlNode 的结构

SqlNode 是 MyBatis 中定义的接口,它只定义了一个方法:

public interface SqlNode {
  boolean apply(DynamicContext context);
}

该方法会根据传入参数,解析 SqlNode 记录的动态 SQL 语句,这个方法我们会在 MyBatis 应用程序执行 SQL 语句的内容中再深入的了解。SqlNode 有 10 个实现类,如下:MyBatis映射器文件解析:动态SQL语句大家好,我是王有志。今天我们来学习 MyBatis 应用程序解析动态 SQL下面我们就来了解每个 SqlNode 的实现类的类型声明和构造方法。

MixedSqlNode

MixedSqlNode 的类型声明,成员变量和构造方法如下:

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
}

MixedSqlNode 是一条 MyBatis 映射器中 SQL 语句解析后的所有 SqlNode 实例的集合,它使用 contents 字段记录了所有 SQL 语句中子元素对应的 SqlNOde 实例。

StaticTextSqlNode

StaticTextSqlNode 的类型声明,成员变量和构造方法如下:

public class StaticTextSqlNode implements SqlNode {
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }
}

StaticTextSqlNode 中使用 text 字段记录了非动态 SQL 语句的内容,MyBatis 认为不含有 trim 元素,where 元素,set 元素,foreach 元素, if 元素,choose 元素,when 元素,otherwise 元素和 bind 元素的 SQL 语句都是非动态 SQL 语句。

TextSqlNode

TextSqlNode 的类型声明,成员变量和构造方法如下:

public class TextSqlNode implements SqlNode {
  private final String text;
  private final Pattern injectionFilter;

  public TextSqlNode(String text) {
    this(text, null);
  }

  public TextSqlNode(String text, Pattern injectionFilter) {
    this.text = text;
    this.injectionFilter = injectionFilter;
  }
}

与 StaticTextSqlNode 类似,TextSqlNode 中使用了 text 字段记录了非动态 SQL 语句的内容,但它们的差别是,当静态 SQL 语句中出现了占位符“${}”时,使用 TextSqlNode,其余情况使用 StaticTextSqlNode

IfSqlNode

IfSqlNode 的类型声明,成员变量和构造方法如下:

public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  private final String test;
  private final SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }
}

IfSqlNode 用于处理动态 SQL 语句的 if 元素,它声明了 3 个成员变量:

  • String 类型的 test 字段,用于存储 if 元素中 test 属性的内容,该属性中配置的是 OGNL 表达式;
  • ExpressionEvaluator 类型的 evaluator 字段,表达式计算器,用于处理 if 元素中的 OGNL 表达式;
  • SqlNode 类型的 contents 字段,用于记录 if 元素的子元素。

ForEachSqlNode

ForEachSqlNode 的类型声明,成员变量和构造方法如下:

public class ForEachSqlNode implements SqlNode {
  public static final String ITEM_PREFIX = "__frch_";

  private final ExpressionEvaluator evaluator;
  private final String collectionExpression;
  private final Boolean nullable;
  private final SqlNode contents;
  private final String open;
  private final String close;
  private final String separator;
  private final String item;
  private final String index;
  private final Configuration configuration;

  public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.nullable = nullable;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
  }
}

ForEachSqlNode 用于处理动态 SQL 语句中的 foreach 元素,它声明了 10 个成员变量:

  • ExpressionEvaluator 的 evaluator,表达式计算器,用于计算 foreach 元素的终止条件;
  • String 的 collectionExpression,循环迭代中使用的集合,对应 foreach 元素中的 collection 属性;
  • Boolean 的 nullable,表示是否允许出现 NULL,对应 foreach 元素中的 nullable 属性;
  • SqlNode 的 contents,用于记录 foreach 元素的子元素;
  • String 的 open,要在循环开始前添加的字符串,对应 foreach 元素中的 open 属性;
  • String 的 close,要在循环结束后添加的字符串,对应 foreach 元素中的 close 属性;
  • String 的 separator,集合中每项元素时之间添加的分隔符,对应 foreach 元素中的 separator 属性;
  • String 的 item,当前循环中迭代的元素,如果 foreach 元素循环迭代的是 Map,此时为 Value;
  • String 的 index,当前迭代的次数,如果 foreach 元素循环迭代的是 Map,此时为 Key;
  • Configuration 的 configuration,MyBatis 核心配置文件在 MyBatis 应用程序中的映射。

TrimSqlNode

TrimSqlNode 的类型声明,成员变量和构造方法如下:

public class TrimSqlNode implements SqlNode {

  private final SqlNode contents;
  private final String prefix;
  private final String suffix;
  private final List<String> prefixesToOverride;
  private final List<String> suffixesToOverride;
  private final Configuration configuration;

  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) {
    this.contents = contents;
    this.prefix = prefix;
    this.prefixesToOverride = prefixesToOverride;
    this.suffix = suffix;
    this.suffixesToOverride = suffixesToOverride;
    this.configuration = configuration;
  }
}

TrimSqlNode 用于处理动态 SQL 语句的 trim 元素,它声明了 6 个成员变量:

  • String 类型的 prefix,需要添加的指定前缀,对应 trim 元素的 prefix 属性;
  • String 类型的 suffix,需要添加的指定后缀,对应 trim 元素的 suffix 属性;
  • List\ <\String> 类型的 prefixesToOverride,需要删除的指定前缀,对应 trim 元素的 prefixOverrides 属性;
  • List\ <\String> 类型的 suffixesToOverride,需要删除的指定后缀,对应 trim 元素的 suffixOverrides 属性;
  • SqlNode 类型的 contents,用于记录 trim 元素的子元素;
  • Configuration 类型的 configuration,MyBatis 核心配置文件在 MyBatis 应用程序中的映射。

通过 trim 元素生成的 SQL 语句片段,会在生成的 SQL 语句片段前后删除 prefixOverrides 字段和 suffixOverrides 字段配置的内容,并在生成的 SQL 语句片段前后添加 prefix 字段和 suffix 字段配置的内容,这点我们会在后面聊到 TrimSqlNode#apply 方法时再和大家详细分享实现过程。

SetSqlNode

SetSqlNode 的类型声明,成员变量和构造方法如下:

public class SetSqlNode extends TrimSqlNode {

  private static final List<String> COMMA = Collections.singletonList(",");

  public SetSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }
}

SetSqlNode 继承自 TrimSqlNode,用于处理动态 SQL 语句中的 set 元素。SetSqlNode 指定了成员变量 prefix 字段为 “SET”,suffixesToOverride 字段为“,”,suffix 字段和 prefixesToOverride 字段为 null。也就是说,使用 set 元素生成的 SQL 语句片段中,如果以“,”结尾,MyBatis 会主动删除“,”,并在 SQL 语句片段的开头添加上“SET”。例如:

<update id="updateByItemId" parameterType="com.wyz.entity.OrderItemDO">  
  update order_item  
  <set>  
    <if test="orderItem.commodityId != null">  
      commodity_id = #{orderItem.commodityId, jdbcType=INTEGER},  
    </if>  
    <if test="orderItem.commodityPrice != null">  
      commodity_price = #{orderItem.commodityPrice, jdbcType=DECIMAL},  
    </if>  
    <if test="orderItem.commodityCount != null">  
      commodity_count = #{orderItem.commodityCount, jdbcType=INTEGER},  
    </if>  
  </set>  
  where item_id = #{orderItem.itemId, jdbcType=INTEGER}  
</update>

如果上面 set 元素中的 if 元素判断均不为 null,则 set 元素中生成的 SQL 语句片段为:

set  commodity_id = #{orderItem.commodityId, jdbcType=INTEGER}, 
     commodity_price = #{orderItem.commodityPrice, jdbcType=DECIMAL}, 
     commodity_count = #{orderItem.commodityCount, jdbcType=INTEGER}	

生成的完成 SQL 语句如下:

update order_item  
set  commodity_id = #{orderItem.commodityId, jdbcType=INTEGER}, 
     commodity_price = #{orderItem.commodityPrice, jdbcType=DECIMAL}, 
     commodity_count = #{orderItem.commodityCount, jdbcType=INTEGER}	
where item_id = #{orderItem.itemId, jdbcType=INTEGER}  

WhereSqlNode

WhereSqlNode 的类型声明,成员变量和构造方法如下:

public class WhereSqlNode extends TrimSqlNode {

  private static final 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);
  }
}

WhereSqlNode 继承自 TrimSqlNode,用于处理动态 SQL 语句中的 where 元素。WhereSqlNode 指定成员变量 prefix 字段为 “where”,prefixesToOverride 字段为 "AND " 和 "OR ",suffix 字段和 suffixesToOverride 字段为 null。也就是说,使用 where 元素生成的 SQL 语句片段中,如果以“AND”或“OR”开头,MyBatis 会主动删除“AND”或“OR”,并在 SQL 语句片段的开头添加上 “where”。例如:

<select id="selectByItemIdAndOrderId" resultMap="BaseResultMap">  
  select * from order_item  
  <where>  
    <if test="orderItem.itemId != null">  
      and item_id = #{orderItem.itemId, jdbcType=INTEGER}  
    </if>  
    <if test="orderItem.orderId != null">  
      and order_id = #{orderItem.orderId, jdbcType=INTEGER}  
    </if>  
  </where>  
</select>

如果上面 where 元素中的 if 元素判断均不为 null,则 where 元素中生成的 SQL 语句片段为:

where item_id = #{orderItem.itemId, jdbcType=INTEGER}  and order_id = #{orderItem.orderId, jdbcType=INTEGER}  

生成的完成 SQL 语句如下:

select * from order_item where item_id = #{orderItem.itemId, jdbcType=INTEGER}  and order_id = #{orderItem.orderId, jdbcType=INTEGER}  

从上面的内容也可以看到,set 元素和 where 元素只是内置了一些固定配置的 trim 元素,以此来实现在特定的 SQL 语句中简化 trim 元素配置的目的

ChooseSqlNode

ChooseSqlNode 的类型声明,成员变量和构造方法如下:

public class ChooseSqlNode implements SqlNode {
  private final SqlNode defaultSqlNode;
  private final List<SqlNode> ifSqlNodes;

  public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    this.ifSqlNodes = ifSqlNodes;
    this.defaultSqlNode = defaultSqlNode;
  }
}

ChooseSqlNode 用于处理动态 SQL 语句的 choose 元素,ChooseSqlNode 中声明了两个成员变量:

  • SqlNode 类型的 defaultSqlNode,where 元素的子元素 otherwise 元素对应的 SqlNode;
  • List<\SqlNode>类型的 ifSqlNodes,where 元素的子元素 when 元素对应的 SqlNode。

VarDeclSqlNode

VarDeclSqlNode 的类型声明,成员变量和构造方法如下:

public class VarDeclSqlNode implements SqlNode {

  private final String name;
  private final String expression;

  public VarDeclSqlNode(String name, String exp) {
    this.name = name;
    this.expression = exp;
  }
}

VarDeclSqlNode 用于处理动态 SQL 语句的 bind 元素,它声明了两个成员变量:

  • String 类型的 name,用于声明变量,对应 bind 元素的 name 属性;
  • String 类型的 expression,用于记录 OGNL 表达式,对应 bind 元素的 value 属性。

由于 choose 元素和 bind 元素在日常的工作中使用的场景较少,因此可能有部分小伙伴忘记了它俩的具体用法,忘记的小伙伴可以回顾下 《MyBatis映射器:实现动态SQL语句》

构建 DynamicSqlSource

public class DynamicSqlSource implements SqlSource {

  private final Configuration configuration;
  private final SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }
}

这部分非常简单,结合前面的内容可以得知在调用 XMLScriptBuilder#parseScriptNode 方法创建 DynamicSqlSource 实例时,传入的 SqlNode 参数是由 XMLScriptBuilder#parseDynamicTags 方法解析 MyBatis 映射器中 SQL 语句生成的 MixedSqlNode 实例。也就是说,DynamicSqlSource 实例中并没有完整的 SQL 语句,只是由 SQL 语句的“碎片”(不同的 SqlNode 实例)和 MyBatis 核心配置文件在 MyBatis 应用程序中的映射 Configuration 实例组成。由于在 MyBatis 映射器文件解析的过程中,不会涉及到更多关于 SqlNode 和 SqlSoucre 的内容,因此这部分我们到这里就结束了。在后面涉及到 MyBatis 应用程序执行 SQL 语句的源码分析中我们还会再来聊 SqlNode 和 SqlSource 的。下一篇文章,我们就正式进入 MyBatis 应用程序执行 SQL 语句的源码分析了。


MyBatis映射器文件解析:动态SQL语句大家好,我是王有志。今天我们来学习 MyBatis 应用程序解析动态 SQL

转载自:https://juejin.cn/post/7399494230677225507
评论
请登录