Mybatis动态sql语句的生成

在XMLLanguageDriver的createSQLSource()方法中,可以将已经经过解析的xml节点传入,并且传入相应的参数类型,开始动态sql语句的生成。

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  return builder.parseScriptNode();
}

 

这里,根据每一个xml节点生成了新的XMLScriptBuilder,可以看到其构造方法。

 

public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
  super(configuration);
  this.context = context;
  this.parameterType = parameterType;
  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());
}

这里除了将所要解析的xml结构数据和参数类型赋值之后,初始化保存了Handler的map,可以看到里面的Handler,顾名思义,不同的xml标签的动态sql语句将交由这里的Handler进行解析。

public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource = null;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
  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) {
      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) { // issue #628
      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);
}

 

在完成XMLScriptBuilder的生成之后,将会调用其parseScriptNode()方法开始对其进行解析。

 

在其中会直接通过parseDynamicTags()方法开始对xml解析后的Node开始具体解析成sqlNode。可以看到,首先会根据获取该节点下面的所有节点一次解析。首先会判断是否是text文本或者是CDATA类型的节点,那么会直接创建成TextSqlNode,其构造方法很简单,只是简单的把文本给赋值在TextSqlNode当中,之后调用isDynamic()方法来判断该节点是否是动态节点,实现如下。

public boolean isDynamic() {
  DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
  GenericTokenParser parser = createParser(checker);
  parser.parse(text);
  return checker.isDynamic();
}
private GenericTokenParser createParser(TokenHandler handler) {
  return new GenericTokenParser("${", "}", handler);
}

 

这里首先会构建一个checker,checker主要来记录该TextSqlNode的是否动态的状态,之后将其作为参数作为createParser()方法的参数传入,这个方法构建了一个GenericTokenParser来做具体的动态评判,可以看到这里将中括号作为解析是否动态的符号。

 

最后通过parse()方法对具体的文本开始解析。

public String parse(String text) {
  if (text == null || text.isEmpty()) {
    return "";
  }
  // search open token
  int start = text.indexOf(openToken, 0);
  if (start == -1) {
    return text;
  }
  char[] src = text.toCharArray();
  int offset = 0;
  final StringBuilder builder = new StringBuilder();
  StringBuilder expression = null;
  while (start > -1) {
    if (start > 0 && src[start - 1] == '\\') {
      // this open token is escaped. remove the backslash and continue.
      builder.append(src, offset, start - offset - 1).append(openToken);
      offset = start + openToken.length();
    } else {
      // found open token. let's search close token.
      if (expression == null) {
        expression = new StringBuilder();
      } else {
        expression.setLength(0);
      }
      builder.append(src, offset, start - offset);
      offset = start + openToken.length();
      int end = text.indexOf(closeToken, offset);
      while (end > -1) {
        if (end > offset && src[end - 1] == '\\') {
          // this close token is escaped. remove the backslash and continue.
          expression.append(src, offset, end - offset - 1).append(closeToken);
          offset = end + closeToken.length();
          end = text.indexOf(closeToken, offset);
        } else {
          expression.append(src, offset, end - offset);
          offset = end + closeToken.length();
          break;
        }
      }
      if (end == -1) {
        // close token was not found.
        builder.append(src, start, src.length - start);
        offset = src.length;
      } else {
        builder.append(handler.handleToken(expression.toString()));
        offset = end + closeToken.length();
      }
    }
    start = text.indexOf(openToken, offset);
  }
  if (offset < src.length) {
    builder.append(src, offset, src.length - offset);
  }
  return builder.toString();
}

 

这里会寻找中括号的两边,如果能够找到,则 说明这里的TextSQLNode是动态的,则会在之前刚刚创建的checker里设置为动态。

 

在回到parseDynamic()方法,根据之前是否为动态,创建TextSQLNode或者StaticTextSQLNode加入在context中。然而,如果该xml的节点的类型如果为element,则说明还需要进一步的解析。在之前的构造方法中已经调用了init方法,在map里存放了各个动态sql语句里面支持的handler,将会在这里根据节点的name取得,如果取不到,说明采用了非法的名字,在这里会抛出异常,如果取到了,则会在这里通过handleNode()方法处理。

以常用的TrimHandler作为例子。

@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 trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
  targetContents.add(trim);
}

在这里,首先会以这个trim节点作为根节点再一次调用parseDynamicTags()方法,进一步解析下面的子节点得到MixSqlNode。在解析完毕之后,设置相应的前后缀以及需要覆盖的前后缀。

最后,以以上得到的结果作为参数生成TrimSqlNode,其构造方法只是简单的按照上述赋值。而这个SqlNode就是经过xml解析之后得到的抽象sqlNode。

而SetHanler的实现也如下。

@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
  SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
  targetContents.add(set);
}
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);
  }

}

SetSQLNode继承了TrimSQLNode,可以看到,实则其构造方法只调用了 TrimSQLNode的构造方法。只是强制把前缀设为了SET。WhereHandler的实现与set的实现也类似,实则也是trimSqlNode的实现,只是对前后缀进行了强制。

@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则必须要将其中test中的表达式取出,赋值在其构造方法中,但是其构造方法只是简单的赋值,其并未继承自TrimSqlNode。

 

 

以上只是针对几种动态xml的标签的handler实现。在完成所有来自xml的node之后的解析抽象成具体的node之后,具体的sql生成的实现则需要在DynamicSqlSource中调用getBoundSql()方法中,调用根节点的apply方法。

以TextSQLNode的apply()为例子(静态StaticTextSQLNode的apply()方法只是简单的把静态字符串赋值)。

@Override
public boolean apply(DynamicContext context) {
  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
  context.appendSql(parser.parse(text));
  return true;
}

 

这里生成了一个BindingTokenParser,作为GenericTokenParser的Handler参数,与之前的是否动态的区别在于BindingTokenParser的handleToken()实现的更加详细。

 

@Override
public String handleToken(String content) {
  Object parameter = context.getBindings().get("_parameter");
  if (parameter == null) {
    context.getBindings().put("value", null);
  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
    context.getBindings().put("value", parameter);
  }
  Object value = OgnlCache.getValue(content, context.getBindings());
  String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
  checkInjection(srtValue);
  return srtValue;
}

 

这里会从之前的上下文中根据前面parse()方法中得到的中括号之间的name来取得上下文中的参数注入,也就是这里完成了动态参数的注入。之后,与静态方法一样将注入好了的sql语句加在结果结果上下文中。

 

@Override
public boolean apply(DynamicContext context) {
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    contents.apply(context);
    return true;
  }
  return false;
}
public boolean evaluateBoolean(String expression, Object parameterObject) {
  Object value = OgnlCache.getValue(expression, parameterObject);
  if (value instanceof Boolean) {
    return (Boolean) value;
  }
  if (value instanceof Number) {
    return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
  }
  return value != null;
}

而在IfSqlNode里,则会通过ognl来将之前从test中取得的表达式与传入的参数进行验证,如果验证通过,则会调用if节点下面的子节点的apply()完成if逻辑的表达。

@Override
public boolean apply(DynamicContext context) {
  FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
  boolean result = contents.apply(filteredDynamicContext);
  filteredDynamicContext.applyAll();
  return result;
}

而TrimSqlNode的apply方法则较为复杂,首先会根据传入的上下文生成FilterDynamicContext,并调用其下所有子节点的apply()方法,并将生成的sql结果存放在FilteredDynamicContext中。

public FilteredDynamicContext(DynamicContext delegate) {
  super(configuration, null);
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (!prefixApplied) { prefixApplied = true; if (prefixesToOverride != null) { for (String toRemove : prefixesToOverride) { if (trimmedUppercaseSql.startsWith(toRemove)) { sql.delete(0, toRemove.trim().length()); break; } } } if (prefix != null) { sql.insert(0, " "); sql.insert(0, prefix); } } }


this.delegate = delegate; this.prefixApplied = false; this.suffixApplied = false; this.sqlBuffer = new StringBuilder();}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());}

 

 

 

最后调用applyAll()方法,根据xml的前后缀配置处理生成结果的前后缀,以applyPrefix()为例子。

 

 

private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!prefixApplied) {
    prefixApplied = true;
    if (prefixesToOverride != null) {
      for (String toRemove : prefixesToOverride) {
        if (trimmedUppercaseSql.startsWith(toRemove)) {
          sql.delete(0, toRemove.trim().length());
          break;
        }
      }
    }
    if (prefix != null) {
      sql.insert(0, " ");
      sql.insert(0, prefix);
    }
  }
}

 

 

逻辑很清晰,动态sql语句的生成基本就如上所示,别的标签实现略有不同,但参照上面的例子都不复杂。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值