Mybatis动态SQL原理解析

2023年6月6日10:07:33

资料准备

官网

        首先翻阅官网文档,(https://mybatis.org/mybatis-3/dynamic-sql.html)这页文档首先描述了Mybatis的动态SQL是多么牛逼,解决了哪些问题,又讲了如何使用,而我们想知道他是如何实现的,所以这不是我们想要的。

Dynamic SQL

One of the most powerful features of MyBatis has always been its Dynamic SQL capabilities. If you have any experience with JDBC or any similar framework, you understand how painful it is to conditionally concatenate strings of SQL together, making sure not to forget spaces or to omit a comma at the end of a list of columns. Dynamic SQL can be downright painful to deal with.

While working with Dynamic SQL will never be a party, MyBatis certainly improves the situation with a powerful Dynamic SQL language that can be used within any mapped SQL statement.

源码        

现在,我们去找mybatis源码(https://github.com/mybatis/mybatis-3/releases/tag/mybatis-3.5.4

源码解析

1.从哪创建的SqlSource

        首先,在Mybatis中,首先从可以看到LanguageDriver接口中的createSqlSource方法,该类其中有2个方法,和2个实现类XMLLanguageDriverRawLanguageDriver,他们的关系如下:

                               

        接下来我们可以看到该接口的其中2个方法:

        1.通过解析XML文件为XNode数据

/**
 * Creates an {@link SqlSource} that will hold the statement read from a mapper xml file. 
 * It is called during startup, when the mapped statement is read from a class or an xml file.
 * 
 * @param configuration The MyBatis configuration
 * @param script XNode parsed from a XML file
 * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
 * @return
 */
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

        2.通过解析注解内容

/**
 * Creates an {@link SqlSource} that will hold the statement read from an annotation.
 * It is called during startup, when the mapped statement is read from a class or an xml file.
 * 
 * @param configuration The MyBatis configuration
 * @param script The content of the annotation
 * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
 * @return 
 */
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

        这两种方式都是为了获得SqlSource对象,第一个方法就是我们常规的通过XML配置SQL语句的方式创建,而第二种是在数据访问层通过对方法加对应注解(eg:@Select)的方式创建。

        而XMLLanguageDriver类实现了这两个方法

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

  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // issue #127
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

        可以看到,在通过尝试使用注解的方式创建SqlSource对象时,仍然会检查是否是包含以“script”为开头的字符串,若包含,则仍会调用XML方式对其进行解析,也就是说,我们可以在注解中加入script头的方式使用动态语句使其使用XML方式解析。当然,不是必要的情况下不建议这么做。

        在方法结尾处,会通过TextSqlNode对象来判定该SqlNode是否是动态的,如果是动态的则使用DynamicSqlSource方式解析,若是静态,则使用RawSqlSource方式解析。

        而在LanguageDriver中,我们也看到了RawLanguageDriver类又继承了XMLLanguageDriver,但它和前者的区别就是RawLanguageDriver仅支持静态语句方式解析。

2.它是怎样解析数据的

        这时,我们回到XMLLanguageDrivercreateSqlSource方法中,它是调用了XMLScriptBuilderparseScriptNode方法来进行解析创建的,而这个方法又做了什么呢?

XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();

        这个方法做了2个事,首先是将SqlNode集合混合,转换为MixedSqlNode对象,再判断是否为动态语句,再来决定是通过DynamicSqlSource还是RawSqlSource解析。

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;
  }

        而我们目前需要关心的是它的parseDynamicTags方法,看它是如何将多个SqlNode转换为MixedSqlNode对象的。

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);
  }

        在上述方法中,会将节点中所有childNode转换为List<SqlNode>对象,再调用节点处理方法。

        首先将当前标签(<SELECT>)中所有子Node获取出来,再检查Node的类型:

        1.若该childNode类型为CDATA节点或静态SQL节点,再检查是否为动态语句,若为动态语句则直接将当前Node加入到SqlNode中,若为静态语句,则使用StaticTextSqlNode对象加入到SqlNode。

        2.若该childNode类型为元素节点(if,choose),则根据节点名从预设节点处理集合中找到Node处理方式,若未从预设节点处理集合中找到对应的处理方法,则抛出构造器异常。

(注意,在这里处理动态SQLNode的时候,会进行handleNode方法的递归调用,用于处理所有嵌套的动态语句)

        现在,我们继续看下去(以IfHandler为例)

MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);

        在处理完所有节点后,会返回一个MixedSqlNode节点,再通过nodeToHandle方法获取到对应的校验字符串(eg :null != a),再将上述节点与该校验字符串混合,生成IfSqlNode对象,最后将IfSqlNode对象加入List<SqlNode>

3.它是如何处理数据的

        在处理数据的时候,就会用到我们上面每个动态语句对应的nodeHandler了,同样,以IfHandler为例,

@Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

        在IfHandler中,它只需要判断True/False,再根据是否结果,决定是否将当前动态语句是否加入到SqlNode中。

        在当前运算中,调用了表达式评估器ExpressionEvaluator的evaluateBoolean方法,传入了表达式与参数值。

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;
  }

        在evaluateBoolean方法中,将表达式通过OgnlCache方法进行判定,若结果是Boolean,则直接返回,若结果是Number,再通过BigDecimal对象的compareTo对象检查,然后返回最终结果。

注意:这里的对象使用的是OgnlCache,打开类描述可以看到这里面对表达式有缓存

 

至此,从动态语句的创建,到解析,到处理完成,过两天下一篇来看看Mybatis的SqlSessionFactory是如何构建的!!!

  • 作者:扶朕去网吧
  • 原文链接:https://blog.csdn.net/pxg943055021/article/details/104732010
    更新时间:2023年6月6日10:07:33 ,共 6223 字。