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

news2025/5/28 14:17:04

大家好,我是王有志。

今天给大家带来的是一道来自京东的 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.coloradmin.cn/o/1671460.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7B2 PRO主题5.4.2免授权直接安装

B2 PRO 5.4.2 最新免授权版不再需要改hosts&#xff0c;直接在wordpress上传安装即可

全域运营是割韭菜吗?看完再下结论!

随着流量时代的到来&#xff0c;各大公私域平台中的流量争夺战日益激烈&#xff0c;商家和品牌实现流量变现的难度值也不断提高&#xff0c;运营人员的压力也逐渐增大。在此背景下&#xff0c;全域运营的兴起或许是一个契机&#xff0c;能够将所有人从内卷的状态中解救出来。而…

基于springboot+vue+Mysql的医疗服务系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

流量卡避坑指南

流量卡避坑指南 在选择流量卡时&#xff0c;有几点需要注意以避免踩坑&#xff1a; 合同期和优惠期。 务必看清楚流量卡的合同期和优惠期。 有些卡可能首月免费&#xff0c;但月底办理可能不划算。 真正的长期套餐应该是优惠期20年以上的。 宣传与实际。 对于所谓的“永久9元…

UV胶的应用场景有哪些?

UV胶是一种特殊的胶水&#xff0c;其固化过程需要紫外光照射。它具有快速固化、高强度、无溶剂挥发等优点&#xff0c;因此在许多应用场景中被广泛使用。UV胶的应用场景非常广泛&#xff0c;包括但不限于以下几个方面&#xff1a; 1.电子产品组装: UV胶在电子产品的组装中扮演…

【iOS逆向与安全】网上gw如何自动登录与签到SM2,SM3,SM4算法加解密

1.下载 app 2.frida 调试 3.抓包查看接口 4.分析加密数据 5.易语言编写代码 1 .开始下载 下载好发现有越狱检测&#xff0c;检测点为&#xff1a; -[AppDelegate isJailBreak]; 于是编写插件xm代码 : %hook AppDelegate- (void)isJailBreak{NSLog("AppDelegate is…

M 有效算法

M 有效算法 本题考验二分知识&#xff0c;思路是二分k的取值&#xff0c;就按第一组样例来说当我们k取值为1的时候我们遍历数组想让|8-x|<k1的话x的取值范围是7-9&#xff0c;想让|3-x|<k2的话x的取值范围是1-5&#xff0c;两者x的区间不重合&#xff0c;说明肯定没有x能…

【c++】二叉搜索树(BST)

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;本篇文章来到二叉搜索树的内容 目录 1.二叉搜索树的介绍2.二叉搜索树的操作与实现insert插入Find查找InOrder中序遍历Erase删除 3.二叉搜索树的应用&#xff08;K…

链动2+1结合消费增值:破解用户留存与复购的密码

大家好&#xff0c;我是吴军&#xff0c;来自一家领先的软件开发公司&#xff0c;担任产品经理的职务。今天&#xff0c;我希望能与大家深入交流链动21模式&#xff0c;特别是它在提升用户留存和复购率方面的独特价值。 虽然链动模式在某些人眼中可能被视为传统或已被超越&…

HCIP的学习(16)

BGP的状态机 ​ OSPF的状态机是在描述整个协议的完整工作过程&#xff0c;而BGP的状态机仅描述的是对等体关系建立过程中的状态变化。-----因为BGP将邻居建立过程以及BGP路由收发过程完全隔离。 ​ IGP协议在启动后&#xff0c;需要通过network命令激活接口&#xff0c;从而使…

运筹系列92:vrp算法包VROOM

1. 介绍 VROOM is an open-source optimization engine written in C20 that aim at providing good solutions to various real-life vehicle routing problems (VRP) within a small computing time. 可以解决如下问题&#xff1a; TSP (travelling salesman problem) CVRP …

三极管 导通条件

一、三极管理解 三极管是电子行业常用的元器件之一&#xff0c;他是一种电流型控制的器件&#xff0c;他有三种工作状态&#xff1a;截止区&#xff0c;放大区、饱和区。当三极管当做开关使用时&#xff0c;他工作在饱和区。下面简短讲解三极管作为开关使用的方法&#xff0c;只…

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

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

C--贪吃蛇

目录 前言 简单的准备工作 蛇的节点 开始前 void GameStart(pSnake ps) void WelcomeToGame() void CreateMap() void InitSnake(pSnake ps) void CreateFood(pSnake ps) 游戏进行 void GameRun(pSnake ps) int NextIsFood(pSnakeNode psn, pSnake ps) void NoFood(pSnak…

whisper之初步使用记录

文章目录 前言 一、whisper是什么&#xff1f; 二、使用步骤 1.安装 2.python调用 3.识别效果评估 4.一点封装 5.参考链接 总结 前言 随着AI大模型的不断发展&#xff0c;语音识别等周边内容也再次引发关注&#xff0c;通过语音转文字再与大模型交互&#xff0c;从而…

ssm125四六级报名与成绩查询系统+jsp

四六级报名与成绩查询系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对四六级报名信息管理混乱&am…

外卖 点金推广实战课程,2024外卖 点金推广全流程(7节课+资料)

课程内容&#xff1a; 外卖点金推广实操课程 资料 01 1-了解外卖.mp4 02 第一节:点金推广的说明.mp4 03 第二节:如何降低点金推广的成本,mp4 04 第三节:如何计算点金推广的流速,mp4 05 第四节:如何提升点金的精准度,mp4 06 第五节:点金推广实操,mp4 07 点金推广高级教程…

UE4_照亮环境_不同雾效的动态切换

一、问题及思路&#xff1a; 我们在一个地图上&#xff0c;经常切换不同的区域&#xff0c;不同的区域可能需要不同的色调&#xff0c;例如暖色调的野外或者幽暗的山洞&#xff0c;这两种环境上&#xff0c;雾效的选用肯定不一样&#xff0c;夕阳西下的户外用的就是偏暖的色调&…

基于微信小程序+JAVA Springboot 实现的【马拉松报名系统】app+后台管理系统 (内附设计LW + PPT+ 源码+ 演示视频 下载)

项目名称 项目名称&#xff1a; 马拉松报名系统微信小程序 项目技术栈 该项目采用了以下核心技术栈&#xff1a; 后端框架/库&#xff1a; Java SSM框架数据库&#xff1a; MySQL前端技术&#xff1a; 微信开发者工具、uni-app其他技术&#xff1a; JSP开发技术 项目展示 …

CANopen总线_CANOpen开源协议栈

CANopen是自动化中使用的嵌入式系统的通信协议栈和设备配置文件规范。就OSI 模型而言&#xff0c;CANopen 实现了以上各层&#xff0c;包括网络层。 CANopen 标准由一个寻址方案、几个小型通信协议和一个由设备配置文件定义的应用层组成。通信协议支持网络管理、设备监控和节点…