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

本文主要是介绍大厂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

这篇关于大厂Java面试题:MyBatis中是如何实现动态SQL的?有哪些动态SQL元素(标签)?描述下动态SQL的实现原理。的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/988111

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

java图像识别工具类(ImageRecognitionUtils)使用实例详解

《java图像识别工具类(ImageRecognitionUtils)使用实例详解》:本文主要介绍如何在Java中使用OpenCV进行图像识别,包括图像加载、预处理、分类、人脸检测和特征提取等步骤... 目录前言1. 图像识别的背景与作用2. 设计目标3. 项目依赖4. 设计与实现 ImageRecogni

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

Java访问修饰符public、private、protected及默认访问权限详解

《Java访问修饰符public、private、protected及默认访问权限详解》:本文主要介绍Java访问修饰符public、private、protected及默认访问权限的相关资料,每... 目录前言1. public 访问修饰符特点:示例:适用场景:2. private 访问修饰符特点:示例:

Mysql虚拟列的使用场景

《Mysql虚拟列的使用场景》MySQL虚拟列是一种在查询时动态生成的特殊列,它不占用存储空间,可以提高查询效率和数据处理便利性,本文给大家介绍Mysql虚拟列的相关知识,感兴趣的朋友一起看看吧... 目录1. 介绍mysql虚拟列1.1 定义和作用1.2 虚拟列与普通列的区别2. MySQL虚拟列的类型2

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

mysql数据库分区的使用

《mysql数据库分区的使用》MySQL分区技术通过将大表分割成多个较小片段,提高查询性能、管理效率和数据存储效率,本文就来介绍一下mysql数据库分区的使用,感兴趣的可以了解一下... 目录【一】分区的基本概念【1】物理存储与逻辑分割【2】查询性能提升【3】数据管理与维护【4】扩展性与并行处理【二】分区的

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超