大厂Java面试题:MyBatis中是如何实现动态SQL的?有哪些动态SQL元素(标签)?描述下动态SQL的实现原理。

大家好,我是王有志

今天给大家带来的是一道来自京东的 MyBatis 面试题:MyBatis 中是如何实现动态 SQL 的?有哪些动态 SQL 元素(标签)?描述下动态 SQL 的实现原理

MyBatis 中提供了 7 个动态 SQL 语句的元素(标签):

  • trim 元素,用于在 MyBatis 映射器中实现 SQL 语句中前后字符串的处理;
  • where 元素,用于在 MyBatis 映射器中实现查询语句中 where 子句的处理;
  • set 元素,用于在 MyBatis 映射器中实现更新语句中 set 子句的处理;
  • if 元素,用于在 MyBatis 映射器中实现类似于 Java 中 if 关键字的条件判断语句;
  • foreach 元素,用于在 MyBatis 映射器中实现集合,字典的遍历;
  • choose 元素,用于在 MyBatis 映射器中实现类似于 Java 的switch...case...default语句中 switch 关键字的功能;
    • when 元素,用于在 MyBatis 映射器中实现类似于 Java 的switch...case...default语句中 case 关键字的功;
    • otherwise 元素,用于在 MyBatis 映射器中实现类似于 Java 的switch...case...default语句中 default 关键字的功;
  • bind 元素,用于在 MyBatis 映射器中声明局部变量的。

网上的很多回答会将 when 元素和 other 元素也计算在内,认为是 9 个动态 SQL 元素(标签),但由于 when 元素与 otherwise 元素必须出现在 choose 元素的内部,因此这里我并没有将它们单独算作是 MyBatis 提供的动态 SQL 元素(标签)。

Tips:关于上述 MyBatis 提供的实现动态 SQL 语句的元素,可以参看我之前的文章《MyBatis映射器:动态 SQL 语句》。

实现原理

简单来说,MyBatis 在处理动态 SQL 元素(标签)分为两个步骤:

  1. 读取 mybaits-config.xml 文件时,会将解析 MyBatis 映射器中的动态 SQL 元素(标签),并存储相应信息
  2. 执行 SQL 语句时,根据传入参数组装动态 SQL 语句,其中 if 元素,when 元素,bind 元素和 foreach 元素中需要使用到 ONGL 表达式计算结果

解析 MyBatis 映射器中的 SQL 语句

解析 SQL 语句环节主要是根据动态 SQL 元素(标签)解析 SQL 语句的配置信息,并存储到 SQL 语句对应的 SqlSource 对象中。

我们先通过一张图来整体的了解下 MyBatis 是解析 SQL 语句的全部流程:

我们从 XMLConfigBuilder 入手,先来看XMLConfigBuilder#parseConfiguration方法的部分源码:

private void parseConfiguration(XNode root) 
  propertiesElement(root.evalNode("properties"));
  Properties settings = settingsAsProperties(root.evalNode("settings"));
  loadCustomVfsImpl(settings);
  loadCustomLogImpl(settings);
  typeAliasesElement(root.evalNode("typeAliases"));
  pluginsElement(root.evalNode("plugins"));
  objectFactoryElement(root.evalNode("objectFactory"));
  objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  reflectorFactoryElement(root.evalNode("reflectorFactory"));
  settingsElement(settings);
  environmentsElement(root.evalNode("environments"));
  databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  typeHandlersElement(root.evalNode("typeHandlers"));
  mappersElement(root.evalNode("mappers"));
}

可以看到,该方法负责调用解析 mybatis-config.xml 文件中每一项配置元素的方法。

其中第 15 行中调用的XMLConfigBuilder#mappersElement方法,是负责解析 MyBatis 映射器文件的,我们继续向下追踪,这里还是用一张调用链路图来展示:

如果你看过我的《大厂Java面试题:MyBatis映射文件中,A元素通过include引入B元素定义的SQL语句,B元素只能定义在A元素之前吗?》,你应该对这段调用链路很熟悉,其中XMLMapperBuilder#configurationElement方法与XMLConfigBuilder#parseConfiguration方法类似,只不过 XMLMapperBuilder 是负责解析 MyBatis 映射器(Mapper.xml)配置元素的,部分源码如下:

private void configurationElement(XNode context) {
  String namespace = context.getStringAttribute("namespace");
  if (namespace == null || namespace.isEmpty()) {
    throw new BuilderException("Mapper's namespace cannot be empty");
  }
  builderAssistant.setCurrentNamespace(namespace);
  cacheRefElement(context.evalNode("cache-ref"));
  cacheElement(context.evalNode("cache"));
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  sqlElement(context.evalNodes("/mapper/sql"));
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}

从源码中不难看出,第 12 行是真正负责解析 MyBatis 映射器中 SQL 语句的方法,接着往下看:

private void buildStatementFromContext(List<XNode> list) {
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    statementParser.parseStatementNode();
  }
}

到这里我们就能看到真正负责解析 MyBatis 映射器中 SQL 语句的方法XMLStatementBuilder#parseStatementNode了,这个方法有 60 多行,在这个问题中我们只需要关注其中创建 SqlSource 对象的这句即可,这段逻辑的调用链路如图:

到这里我们终于看到了解析动态 SQL 元素(标签的)方法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) {
      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) { 
      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#parseDynamicTags方法的核心功能非常简单,解析 SQL 语句中的 XNode 对象,并根据 XNode 对象的类型创建对应的 SqlNode 对象。注意这段代码中,每个 XNode 对象都会生成对应的 SqlNode 对象存放到 contents 中,最后为整个 SQL 语句创建的 MixedSqlNode 对象中持有了 contents。

第 15 行的 else 语句中,当 XNode 对象的类型为Node.ELEMENT_NODE时(即 XML 文档中的元素),通过 nodeHandlerMap 获取对应元素的 NodeHandler 实现进行解析。 NodeHandler 是 XMLScriptBuilder 中的内部类,其实现体系如下:

几乎每个动态 SQL 元素都有自己的 NodeHandle 实现,除了 when 元素,这是因为 when 元素与 if 元素的功能相同,因此可以直接复用 IfNodeHandle 来实现 when 元素的解析,因此在为 when 元素创建 SqlNode 对象时,创建的也是 IfSqlNode 对象。

我们以 IfHandler 为例来分析源码,IfHandler 的部分源码如下:

private class IfHandler implements NodeHandler {

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

第 4 行时递归调用XMLScriptBuilder#parseDynamicTags方法,除了 bind 元素和 choose 元素外,其它元素的 NodeHandle 都会递归调用XMLScriptBuilder#parseDynamicTags方法,这是因为除了 bind 元素和 choose 元素,其它动态 SQL 元素都允许嵌套使用。

第 5 行代码中,解析了 if 元素的 test 属性中的内容(即我们编写的条件判断逻辑),并在第 6 行中创建了 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();
  }
}

只做了参数赋值,并没有其它的动作,不过需要注意第 10 行,这里创建了 ExpressionEvaluator 对象,你先眼熟它,下面我们在分析 SQL 语句执行过程时还会再看到它。

至此,MyBatis 就已经完成了 MyBatis 映射器中 SQL 语句的解析工作,在这部分的处理中,MyBatis 解析了每个 SQL 语句,为每个 XNode 对象创建了对应的 SqlNode 对象,并将它们存储到整个 SQL 语句对应的 SqlSource 对象中

组装 MyBatis 映射器中的 SQL 语句

在为 MyBatis 映射器中每个 SQL 语句创建 SqlSource 对象后,我们就可以执行这些 SQL 语句了。

我们跳过从 Mapper 接口到 Executor 的调用逻辑,直接从BaseExecutor#query的方法开始。注意BaseExecutor#query有多个重载方法,这里我们看的是如下方法:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

我们重点关注处理 SQL 语句的部分,即第 2 行中调用的MappedStatement#getBoundSql方法,这里还是用一张调用链路图来展示:

注意,这里并不是一定会使用 DynamicSqlSource 来处理 SQL 语句,只不过我们在讲动态 SQL 元素(标签),因此在解析过程中创建的一定是 DynamicSqlSource 对象。我们来看DynamicSqlSource#getBoundSql方法的源码:

public BoundSql getBoundSql(Object parameterObject) {
  DynamicContext context = new DynamicContext(configuration, parameterObject);
  rootSqlNode.apply(context);
  SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  context.getBindings().forEach(boundSql::setAdditionalParameter);
  return boundSql;
}

先来看第 2 行代码中创建 DynamicContext 对象调用的构造方法:

public DynamicContext(Configuration configuration, Object parameterObject) {
  if (parameterObject != null && !(parameterObject instanceof Map)) {
    MetaObject metaObject = configuration.newMetaObject(parameterObject);
    boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
    bindings = new ContextMap(metaObject, existsTypeHandler);
  } else {
    bindings = new ContextMap(null, false);
  }
  bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
  bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}

这部分主要是处理调用 Mapper 接口时传入的参数 parameterObject,并将 parameterObject 存储到 DynamicContext 对象的 bindings 中。

接着来看DnamicSqlSource#getBoundSql方法的第 3 行代码,还记得我们前面提到的“为整个 SQL 语句创建的 MixedSqlNode 对象中持有了 contents”吗?这里的 rootSqlNode 就是之前创建的 MixedSqlNode 对象。我们来看MixedSqlNode#apply方法的源码:

public boolean apply(DynamicContext context) {
  contents.forEach(node -> node.apply(context));
  return true;
}

这里就很简单了,遍历 MixedSqlNode 对象的 contents 字段,并调用对应SqlNode#apply方法,这里我们先来看下 SqlNode 的体系:

SqlNode体系.png

上图中并没有出现 when 元素和 otherwise 元素对应的 SqlNode,这是因为它们的处理逻辑全部被封装到 ChoooseSqlNode 里了;而 VarDeclSqlNode 对应的则是 bind 元素;TextSqlNode 和 StaticTextSqlNode 对应的是 XML 中的文本;另外还有 MixedSqlNode,它是负责调用其他类型的 SqlNode 的。

还是以 if 元素对应的 IfSqlNode 为例,来看IfSqlNode#apply方法的源码:

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

第 2 行的代码中,MyBatis 调用ExpressionEvaluator#evaluateBoolean方法通过 DnamicSqlSource 的 bindings 属性(即调用 Mapper 接口时传入的参数)来计算 test 的结果(test 存储的是解析 if 元素中 test 属性的内容,这点我们前面提到过),来看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;
}

可以看到,该方法是通过调用OgnlCache#getValue来计算表达式的结果的,这里使用的 OgnlCache 是 MyBatis 对 ONGL 做的一层封装,我们就不再深入了。

IfSqlNode#apply方法中,根据ExpressionEvaluator#evaluateBoolean方法的计算结果,决定是否将 SQL 语句组装到 DnamicSqlSource 对象中。其它动态 SQL 元素对应的 SqlNode 也是类似的处理逻辑,只是有些动态 SQL 元素并不需要使用 OGNL 表达式,因此 SqlNode 在实现上只是通过 Java 代码进行逻辑处理,并组装到 DnamicSqlSource 对象中。

至此,MyBatis 就已经完成了动态 SQL 语句的拼装,这部分处理中,主要是根据参数计算(OGNL 表达式计算或其他的代码逻辑处理)结果,将动态 SQL 语句拼装到 DnamicSqlSource 对象中


好了,今天的内容就到这里了,如果本文对你有帮助的话,希望多多点赞支持,如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核 Java 技术的金融摸鱼侠王有志,我们下次再见!

qrcode_for_gh_9b072ecdb954_258.jpg

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术范王有志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值