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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【循环程序设计-谭浩强适配】(适合专升本、考研)

无偿分享学习资料&#xff0c;需要的小伙伴评论区或私信dd。。。 无偿分享学习资料&#xff0c;需要的小伙伴评论区或私信dd。。。 无偿分享学习资料&#xff0c;需要的小伙伴评论区或私信dd。。。 完整资料如下&#xff1a;纯干货、纯干货、纯干货&#xff01;&#xff01;…

DIFT:Emergent Correspondence from Image Diffusion # 论文阅读

URL https://arxiv.org/pdf/2306.03881 主页&#xff1a;https://diffusionfeatures.github.io/ 代码&#xff1a;https://github.com/Tsingularity/dift TD;DR 23 年 6月 cornell 大学的文章&#xff0c;任务是做图片的特征匹配&#xff08;关联&#xff09;&#xff0c;特…

004.可观察对象与观察者

Rx非常适合事件驱动的应用程序。这是有意义的&#xff0c;因为事件(作为)(如前所述)是创建时变值的命令式方法。从历史上看,事件驱动编程主要出现在客户端技术中&#xff0c;因为作为事件实现的用户交互。例如&#xff0c;你可能工作过使用OnMouseMove或OnKeyPressed事件。正因…

大模型时代下两种few shot高效文本分类方法

介绍近年(2022、2024)大语言模型盛行下的两篇文本分类相关的论文&#xff0c;适用场景为few shot。两种方法分别是setfit和fastfit&#xff0c;都提供了python的包使用方便。 论文1&#xff1a;Efficient Few-Shot Learning Without Prompts 题目&#xff1a;无需提示的高效少…

C语言——模拟实现库函数atoi

1. atoi atoi&#xff1a;将字符串转换为整数。 1. 头文件 <string.h> 2. 声明&#xff1a; 1. 如果字符串开始有空格&#xff0c;直接跳过。 2. - 会影响打印的数字的正负。 3. 只打印数字字符&#xff0c;遇到非数字字符就停止。 4. 如果转换之后的数字大于 …

开眼了,自动化测试还能这样用?

持续集成的自动化测试通常需要将代码、测试用例与持续集成工具进行绑定&#xff0c;以实现自动运行。然而&#xff0c;Apipost的自动化测试功能需要手动操作&#xff0c;并且需要手动查看测试结果。 为了解决这个问题&#xff0c;Apipost推出了持续集成功能&#xff0c;方便同…

2025考研 | 北京师范大学计算机考研考情分析

北京师范大学&#xff08;Beijing Normal University&#xff09;简称“北师大”&#xff0c;由中华人民共和国教育部直属&#xff0c;中央直管副部级建制&#xff0c;位列“211工程”、“985工程”&#xff0c;入选国家“双一流”、“珠峰计划”、“2011计划”、“111计划”、…

从“金事通”带给我意想不到的来说--“数据是架构的中心”

背景 上周一个保险的销售人员来找我完成一定的售后流程。其中有一项是请我下载一个叫 金事通的 APP。说实在的我根本没听过。她说这是政治任务。我想不是有你们保险公司的APP了嘛。为什么还要我安装。没办法先安装吧。 经历了注册、人脸识别的步骤后。可以登录了。注册短信发…

【异常处理】(中北大学-程序设计基础(2))

目录 题目 源码 结果示例 题目 求一元二次方程式ax^2bxc0的实根&#xff0c;如果方程没有实根&#xff0c;则输入有关警告信息。要求&#xff1a;建立一元二次方程类&#xff0c;利用异常技术处理。 源码 #include <iostream> #include <cmath>using namespa…

iOS MRC那句话

混编时使用MRC文件需要使用这句话 -fno-objc-arc在下图中显示的位置添加

【Android Studio】开启真机调试

1 打开手机的开发者模式 各种款式的手机进入开发者模式的情况不同&#xff0c;但大致是在 【关于手机】中多次点击系统版本即可进入。这里以小米8为例&#xff0c;记录下流程。 1.1 进入手机开发者模式 【设置】->【我的设备】->【全部参数】->【MIUI版本】连续点击3…

防火请技术基础篇:令牌桶机制的剖析与应用

防火墙中的令牌桶机制&#xff1a;深度剖析与应用 在现代网络通信中&#xff0c;防火墙技术发挥着至关重要的作用&#xff0c;它不仅能够实现网络安全防御&#xff0c;还能通过诸如令牌桶算法等机制来有效管理网络流量&#xff0c;保证网络服务的质量。本文将全面深入地探讨防…