大厂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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/621964.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

国际护士节庆祝活动向媒体投稿有方法很轻松

作为一名医院职工,我肩负着医院对外信息宣传的重任。在国际护士节这个特殊的日子里,我们医院举办了一系列庆祝活动,以表彰护士们的辛勤付出和无私奉献。然而,在将这些活动信息投稿至媒体的过程中,我最初却遭遇了诸多挑战。 起初,我采用传统的邮箱投稿方式,将精心撰写的稿件发送…

基于51单片机的电子门铃设计( proteus仿真+程序+设计报告+原理图+讲解视频)

基于51单片机电子门铃设计( proteus仿真程序设计报告原理图讲解视频&#xff09; 仿真图proteus7.8及以上 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;S0091 1. 主要功能&#xff1a; 基于51单片机的智能门铃设计 1、系统采用…

共赴科技盛会“2024南京智博会”11月在南京国际博览中心召开

2024年&#xff0c;南京这座历史悠久的文化名城迎来了一场科技与智慧交织的盛会——南京智博会|南京国际智慧城市、物联网、大数据。本次博览会以智慧城市、人工智能、消费电子、物联网、大数据为主题&#xff0c;汇聚了全球各地的智能科技精英&#xff0c;共同探讨智慧城市建设…

设计模式Java实现-迭代器模式

✨这里是第七人格的博客✨小七&#xff0c;欢迎您的到来~✨ &#x1f345;系列专栏&#xff1a;设计模式&#x1f345; ✈️本篇内容: 迭代器模式✈️ &#x1f371; 本篇收录完整代码地址&#xff1a;https://gitee.com/diqirenge/design-pattern &#x1f371; 楔子 很久…

[华为OD] B卷 树状结构查询 200

题目&#xff1a; 通常使用多行的节点、父节点表示一棵树&#xff0c;比如 西安 陕西 陕西 中国 江西 中国 中国 亚洲 泰国 亚洲 输入一个节点之后&#xff0c;请打印出来树中他的所有下层节点 输入描述 第一行输入行数&#xff0c;下面是多行数据&#xff0c;每行以空…

MySQL 大量数据插入优化

效率最好的方式是&#xff1a;批量插入 开启事务。 1、数据批量插入相比数据逐条插入的运行效率得到极大提升&#xff1b; ## 批量插入 INSERT INTO table (field1, field12,...) VALUES (valuea1, valuea2,...), (valueb1, valueb2,...),...;当数据逐条插入时&#xff0c;每…

145.二叉树的后序遍历

刷算法题&#xff1a; 第一遍&#xff1a;1.看5分钟&#xff0c;没思路看题解 2.通过题解改进自己的解法&#xff0c;并且要写每行的注释以及自己的思路。 3.思考自己做到了题解的哪一步&#xff0c;下次怎么才能做对(总结方法) 4.整理到自己的自媒体平台。 5.再刷重复的类…

【优选算法】——Leetcode——202—— 快乐数

目录 1.题目 2. 题⽬分析: 3.简单证明&#xff1a; 4. 解法&#xff08;快慢指针&#xff09;&#xff1a; 算法思路&#xff1a; 补充知识&#xff1a;如何求⼀个数n每个位置上的数字的平⽅和。 总结概括 5.代码实现 1.C语言 2.C 1.题目 202. 快乐数 编写一个算法来…

Scala、Spark SQL 常用方法

目录 数组常用方法 列表操作常用方法 Scala中常用的查看列表元素的方法有head、init、last、tail和take()。 合并两个列表还可以使用concat()方法。 集合操作常用方法 map()方法 foreach()方法 filter()方法 flatten()方法 groupBy()方法 ​编辑 从内存中读取数据创建…

【Python技术】使用akshare、pandas高效复盘每日涨停板行业分析

作为一个程序员宝爸&#xff0c;每天的时间很宝贵&#xff0c;工作之余除了辅导孩子作业&#xff0c;就是补充睡眠。 怎么快速高效的进行当天A股涨停板的复盘&#xff0c;便于第二天的跟踪。这里简单写个示例&#xff0c; 获取当天连涨数排序&#xff0c;以及所属行业排序。 …

详解依赖注入的三种方法以及遇到问题的解决

各位大佬光临寒舍&#xff0c;希望各位能赏脸给个三连&#xff0c;谢谢各位大佬了&#xff01;&#xff01;&#xff01; 目录 1.三种依赖注入的方法 1.属性注入 优点 缺点 2.构造方法注入 优点 缺点 3.Setter注入 优点 缺点 4.小结 2.依赖注入常见问题的解决 1…

人工智能中的概率魔法:解锁不确定性的智慧之钥

在人工智能&#xff08;AI&#xff09;的广阔天地中&#xff0c;概率论以其独特的魅力&#xff0c;成为了连接现实世界与智能决策的桥梁。从语音识别到图像识别&#xff0c;从自然语言处理到机器翻译&#xff0c;从智能推荐到自动驾驶&#xff0c;概率论知识在这些领域中发挥着…

ONVIF系列三:ONVIF客户端实现

ONVIF系列&#xff1a; ONVIF系列一&#xff1a;ONVIF介绍 ONVIF系列二&#xff1a;Ubuntu安装gSOAP、生成ONVIF代码框架 ONVIF系列三&#xff1a;ONVIF客户端实现 在系列二中完成了在Ubuntu上安装gSOAP并生成ONVIF代码框架&#xff0c;接下来我们利用生成的框架实现ONVIF客户端…

Spring框架核心:揭秘Java厨房的智能烹饪艺术

前情回顾&#xff1a;Spring框架深度解析&#xff1a;打造你的Java应用梦工厂 六. 实现控制反转 6.1 描述如何在Spring中实现IoC 在Spring Town的厨房里&#xff0c;实现控制反转就像是将食材的采购和准备过程外包给了一个智能系统。这个系统知道每种食材的特性&#xff0c;也…

质量保障之精准测试!

一、背景与概念 随着软件测试行业的长足发展&#xff0c;测试理念、技术都在发生着日新月异的变化。因此一套完整的自动化测试用例对于每个软件公司都是不可或缺的&#xff0c;然而虽然有如此规模宏大的自动化案例集资源投入&#xff0c;同时也有大量人力的投入&#xff0c;但…

深入理解Python的类,实例和type函数

问题起源&#xff1a; class t():pass s1 t() s2 type("Student2",(),{}) isinstance(s1, type), isinstance(s2, type)为什么第一个是false&#xff0c;第二个是true呢 根因定位&#xff1a; 在Python中&#xff0c;一切皆对象&#xff0c;类是对象&#xff0c…

AI+新能源充电桩数据集

需要的同学私信联系&#xff0c;推荐关注上面图片右下角的订阅号平台 自取下载。 随着我国新能源汽车市场的蓬勃发展&#xff0c;充电桩的需求量日益增加&#xff0c;充电桩的智能化程度不仅影响充电站运营商的经营效益&#xff0c;也大大影响着用户的充电体验。AI技术可以涵盖…

STK12 RPO模块学习 (1)

一、背景介绍 在STK12中&#xff0c;在Astrogator的模块上开发了新的模块&#xff08;Rendezvous and proximity operations)。轨道交会接近通常来说是一个很复杂的过程。RPO实现需要对轨道动力学有一个清晰的理解&#xff0c;并且对于Astrogator模块具备很强的背景和经验&…

AI翻唱+视频剪辑全流程实战

目录 一、AI翻唱之模型训练 &#xff08;1&#xff09;模型部署 &#xff08;2&#xff09;数据集制作——搜集素材 &#xff08;3&#xff09;数据集制作——提升音频质量 方法一&#xff1a;使用RVC提供的音频处理功能。 方法二&#xff1a;可以使用音频剪辑工具Ad…

【软设】常见易错题汇总

目录 计算机系统基础 程序语言基础 数据结构 算法设计与分析 计算机网络与信息安全 软件工程基础 开发方法&#xff08;结构化与面向对象&#xff09; 数据库 操作系统 知识产权相关的法律法规 &#x1f92f;&#x1f92f;&#x1f92f;&#x1f92f;&#x1f92f;&#x1f9…