mybatis源码解读(五):XMLScriptBuilder详解-各种SQLNODE

本文深入探讨MyBatis框架中动态SQL的解析机制,包括XMLScriptBuilder类的作用,SQLNode的设计理念,以及动态标签如foreach、if、choose等的处理流程。解析过程涉及SQL动态与静态的区分,动态SQL的标签解析,以及最终SQL源的生成。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

功能

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦,诸如 foreach、set这些标签,在我们写sql的时候会提供很大的方便。
mybatis 解析动态sql的这种能力是由 XMLScriptBuilder 类完成的。XMLScriptBuilder 的解析流程:
第一步:解析sql,区别当前的sql是静态文本sql还是带标签的动态sql,然后将其解析成为对应的sqlnode,放到 MixedSqlNode 这个类的 contents 中。
第二步:根据是否是动态sql去生成不同的sqlSource,如果是动态sql则是 DynamicSqlSource,如果不是则是 RawSqlSource。

UML

SQLNode是mybatis很有特色的一个设计,他将一些复杂的东西拆分,然后根据不同的职责形成一个责任链来处理,每个sqlNode就是这个责任链中的具体处理者。mybatis将每一个动态SQL的标签都对应了一个SQLnode去处理。
我们先来看一下 sqlNode的UML图
在这里插入图片描述
可以看到,是有十个实现类的

  • ChooseSqlNode ----- 对应 choose 标签
  • ForEachSqlNode ----- 对应 foreach 标签
  • IfSqlNode ----- 对应 if 、when标签
  • SetSqlNode ----- 对应 set 标签
  • StaticTextSqlNode ----- 对应 完全的纯文本SQL
  • TextSqlNode ----- 对应 普通SQL但是 包含 $ {} 这种的
  • TrimSqlNode ----- 对应 trim标签
  • VarDeclSqlNode ----- 对应 bind 标签
  • WhereSqlNode ----- 对应 where 标签

这些节点基本涵盖了我们能够看到的标签
除了sqlNode之外,在XMLScriptBuilder中还有一个NodeHandler,这个是将对应标签处理成为对应de node
在这里插入图片描述

代码解析

首先在parseScriptNode()方法中,最重要的应该就是parseDynamicTags()以及 构建sqlSource这两步了

public SqlSource parseScriptNode() {
    // 解析SQL 动态SQL
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

parseDynamicTags()的方法逻辑其实还是很简单的,首先会获取到当前标签的所有子节点,然后判断当前子节点是不是普通文本sql,如果是会根据具体是否包含 ${} 这种符号来决定是StaticTextSqlNode 还是TextSqlNode 类型,至于如果不是,孩子节点是标签,则会直接通过handler去处理成为对应的sqlNode。

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));
      // 如果是 普通sql 则走这个case
      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));
        }
      // 如果是标签,则走这里,也就是动态SQL
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        // 会根据nodeName去拿typehandler
        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);
  }

我们可以看看 textSqlNode.isDynamic() 这个方法是做如何识别是否包含 ${} 符号的,在isDynamic()的 方法逻辑中,与之前替换 ${} 符号的逻辑相似,都有 GenericTokenParser 这个类,然后我们主要看 createParser() 方法,可以发现,他将 ${} 填入了进去,之前替换 ${}的时候是从上下文变量中取的,这次则是制作检查,如果包含则会将 isDynamic 设为true。

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

NodeTypeHandler的替换就是通过nodeHandlerMap去查找相对应的NodeTypeHandler,然后调用handleNode()方法,nodeHandlerMap是个map容器,赋值的时候就是穷举了一些handler并且注入其中

        NodeHandler handler = nodeHandlerMap.get(nodeName);
        handler.handleNode(child, contents);

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

handler.handleNode()的逻辑是在处理每个动态标签的一些属性,然后返回一个SqlNode,我们看一下TrimHandler的做法,可以发现他会去找 prefix、prefixOverrides、suffix等标签,然后生成一个TrimSqlNode

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

处理完动态标签之后,就是创建Sqlsource了,动态SQL的sqlsource创建其实是很简单的,就是一个构造方法,但是如果是普通sql,这里有个很重要的处理步骤,就是替换 # {} 。RawSqlSource的构造方法中,有个重要的地方,就是会将parameterType 入参类型做个check并且会去使用 configuration。

 public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

然后在parse(0方法中,看到了我们熟悉的一个类 GenericTokenParser 以及 # {} 这种标签,这里sql会被最终解析成为 select id,name,age from user where id = ? 这种样子,这个是mysql的preparestatement需要的格式,mybatis默认是 通过 preparestatement 去请求数据库的,这个之前介绍过。

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

将sqlsource解析完之后,其实mybatis的整个解析流程就到这里结束了,我们回到XMLStatementBuilder的parseStatementNode()方法中,可以看到最终会通过builderAssistant.addMappedStatement 将我们所解析出来的这一堆东西全部都放入到configuration中,注意,这里一个CURD标签就对应着一个statement。

SQLNODE

介绍一下各个sqlNode的具体解析逻辑,工作逻辑会在executor那一块中介绍。
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);
    }

WhereHandler
对于where这种标签来说,更重要的是where里边的逻辑,所以他会去递归调用一下,将全部标签解析完成

@Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
      targetContents.add(where);
    }

SetHandler
set其实与where是相同的道理

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

ForEachHandler
foreach的话,同样也需要去解析内部的标签,当然也需要去解析一些属性,可以看到,foreach中解析的属性都是我们平时经常见的。

@Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      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");
      ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
      targetContents.add(forEachSqlNode);
    }

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);
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值