返回顶部
首页 > 资讯 > 后端开发 > Python >关于Mybatis中SQL节点的深入解析
  • 653
分享到

关于Mybatis中SQL节点的深入解析

2024-04-02 19:04:59 653人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

目录一、文章引出原因二、存在的问题三、分析 sql 生成过程四、分析多余 SQL 的生成五、解决办法六、总结一、文章引出原因 某天在完成项目中的一个小功能后进行自测的时候,发现存在一

一、文章引出原因

某天在完成项目中的一个小功能后进行自测的时候,发现存在一个很奇怪的 bug --- 最终执行的 SQL 与我所期望的 SQL 不一致,有一个 if 分支在我不传特定参数的情况下被拼接在最终的 SQL 上。

①定义在 XML 文件中的 SQL 语句

<select id="balanceByUserIds" parameterType="xxx.BalanceReqVO" 
resultType="xxx.Balance">
        select * from balance
        <where>
            <if test="dataOrGCodes != null and dataOrgCodes.size > 0">
                and data_org_code in
                <foreach collection="dataOrgCodes" open="(" separator="," close=")" item="dataOrgCode">
                    #{dataOrgCode}
                </foreach>
            </if>
            <if test="dataOrgCode != null and dataOrgCode != ''">
                and data_org_code = #{dataOrgCode}
            </if>
        </where>
    </select>

②传进来的参数

{
    "dataOrgCodes":["6","2"]
}

mybatis 打印执行的 SQL

SELECT
	*
FROM
	balance
WHERE
	data_org_code IN (?, ?)
AND data_org_code = ?

打印的执行参数

{
    "dataOrgCodes":["6","2"]
}

二、存在的问题

学过 Mybatis 的人应该一样就看出来了,这个 SQL 不对劲,多了一些不该有的东西。按照我们的理解,最终的执行的 SQL 应该是

SELECT
	*
FROM
	balance
WHERE
	data_org_code IN (?, ?)

但 mybatis 执行的 SQL 多了一点语句---AND data_org_code = ?

在出现这个问题后我反复进行 debug,确定了自己传进来的参数没有什么问题,也没有什么拦截器添加多余的参数。

三、分析 SQL 生成过程

在确定编写 XML 文件的 if 标签的内容以及传进来的参数无误后,排除了参数导致问题。那么除了这个可能外,问题就可能出现在 SQL 的解析上,也就是 SQL 的生成那里。那么我们定位到 SQL 的生成地方, DynamicSqlSource#getBoundSql(我们查询的参数对象)方法

// Configuration是Mybatis核心类,rootSqlnode 根SQL节点是我们定义在XML中的SQL语句。
//(例如<select>rootSqlNode</sselect>, 标签中间的内容就是 rootSqlNode)
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
}

public BoundSql getBoundSql(Object parameterObject) {
  DynamicContext context = new DynamicContext(configuration, parameterObject);
  rootSqlNode.apply(context);
  ..............................
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  context.getBindings().forEach(boundSql::setAdditionalParameter);
  return boundSql;
}

可以看到方法内部显示创建了一个 DynamicContext,这个对象就是用于存储动态生成的 SQL。

(下面是省略了很多关于本次问题无关的代码,只保留有关代码)

public class DynamicContext {

  public static final String PARAMETER_OBJECT_KEY = "_parameter";
  public static final String DATABASE_ID_KEY = "_databaseId";
	
  // 存储动态生成的SQL,类似于 StringBuilder 的角色
  private final StringJoiner sqlBuilder = new StringJoiner(" ");
  // 唯一编号值,会在生成最终SQL和参数值映射关系的时候用到
  private int uniqueNumber = 0;

  // 拼接SQL
  public void appendSql(String sql) {
    sqlBuilder.add(sql);
  }

  // 获取拼接好的SQL
  public String getSql() {
    return sqlBuilder.toString().trim();
  }

  // 获取唯一编号,返回后进行加一
  public int getUniqueNumber() {
    return uniqueNumber++;
  }
}

而下一句就是解析我们编写的 SQL,完成 SQL 的拼接

rootSqlNode.apply(context)

这里的 rootSqlNode 是我们编写在标签里的 SQL 内容,包括<if>、<foreach>、<where>标签等内容。

rootSqlNode 对象是 SqlNode 类型。其实这里的 SQL 语句被解析成类似于 html 的 DOM 节点的树级结构,在本节的测试例子中结构类似如下(不完全正确,只做参考价值,表示 rootSqlNode 结构类似于以下结构):

<SqlNode>
  	select * from balance
    <SqlNode>
        where
        <SqlNode>
            and data_org_code in
            <SqlNode>
               #{dataOrgCode}
            </SqlNode>
        </SqlNode>
      	<SqlNode>
            and data_org_code =
            <SqlNode>
               #{dataOrgCode}
            </SqlNode>
        </SqlNode>
    </SqlNode>
</SqlNode>

这个 SqlNode 定义如下所示:

public interface SqlNode {
  boolean apply(DynamicContext context);
}

里面的 apply 方法是用于评估是否把这个 SqlNode 的内容拼接到最终返回的 SQL 上的,不同类型的 SqlNode 有不同的实现,例如我们本节相关的 SqlNode 类型就是为 IfSqlNode,对应这我们写的 SQL 语句的 if 标签,以及存储最终的 sql 内容的 StaticTextSqlNode 类型。

public class StaticTextSqlNode implements SqlNode {
  // 存储我们写的 sql 
  // 类似于 and data_org_code in
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }

  @Override
  public boolean apply(DynamicContext context) {
    // 调用 DynamicContext 对象的 sqppendSql 方法拼接最终 sql
    context.appendSql(text);
    return true;
  }

}
public class IfSqlNode implements SqlNode {
  // 评估器
  private final ExpressionEvaluator evaluator;
  // if标签中用于判断这个语句是否生效的 test 属性值
  // 这里对应我们例子中的一个为 "dataOrgCodes != null and dataOrgCodes.size > 0"
  private final String test;
  // if标签中的内容,如果if标签中不存在其他标签,那么这里的值就是StaticTextSqlNode类型的节点
  // StaticTextSqlNode 节点的 text 属性就是我们最终需要拼接的 sql 语句
  private final SqlNode contents;
	
  // contents 是我们定义在 if 标签里面的内容, test 是 if 标签的属性 test 定义的内容
  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

  @Override
  public boolean apply(DynamicContext context) {
    // 使用评估器评估 if 标签中定义的 test 中的内容是否为true
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      // 当contents为StaticTextSqlNode类型的节点时候,就把 if 标签里的内容拼接到 sql 上
      // 否则继续调用方法 apply(相当于递归调用,知道找到最下面的内容节点)
      contents.apply(context);
      return true;
    }
    return false;
  }

}

我们可以看到这里的

evaluator.evaluateBoolean(test, context.getBindings())

这个评估方法是通过把 test 语句内容和 我们传进来的参数解析出来的 Map 进行比对,如果我们的参数中存在值,且值得内容符合 test 语句的判断,则进行 sql 语句的拼接。例如本次例子中的

<if test="userIds != null and userIds.size > 0">
    and data_org_code in
    <foreach collection="dataOrgCodes" open="(" separator="," close=")" item="dataOrgCode">
         #{dataOrgCode}
    </foreach>
</if>

以及我们传进来的参数进行比对

{
    "dataOrgCodes":["6","2"]
}

可以看得出来参数与 test 语句 "dataOrgCodes!= null and dataOrgCodes.size > 0" 比较是返回 true 的。

四、分析多余 SQL 的生成

根据上面的执行步骤可以知道,我们的 bug 的产生是在

evaluator.evaluateBoolean(test, context.getBindings()) 这一步产生的。也就是在 context.getBindings() 中存在满足 dataOrgCode != null and dataOrgCode != '' 的属性。debug 验证以下可知

可以看得出来,存储参数映射的 Map 出现了 dataOrgCode 的属性,但是我们传递进来的属性只有 dataOrgCodes 数组,没有 dataOrgCode 属性,那这个 dataOrgCode 属性是怎么来的?

再次从头进行 debug 发现问题出现在 ForEachSqlNode 的 apply 方法里面

public boolean apply(DynamicContext context) {
  // 获取参数映射存储Map
  Map<String, Object> bindings = context.getBindings();
  // 获取bingdings中的parameter参数,key为collectionExpression,也就是我们写在标签foreach 标签的 collection 值里的内容
  // 根据collectionExpression从参数映射器中获取到对应的值, 本次的值为:["1","2"]
  final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
    Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
  if (iterable == null || !iterable.iterator().hasNext()) {
    return true;
  }
  // 第一个参数
  boolean first = true;
  // 再拼接sql里添加我们定义在 foreach 标签的 open 值里的内容
  applyOpen(context);
  // 遍历的计数器
  int i = 0;
  // 遍历我们传进来的数组数据 ["1","2"]
  // o 表示我们本次遍历数组中的值,例如 ”1“
  for (Object o : iterable) {
    DynamicContext oldContext = context;
    if (first || separator == null) {
      context = new PrefixedContext(context, "");
    } else {
      context = new PrefixedContext(context, separator);
    }
    int uniqueNumber = context.getUniqueNumber();

    // 把 foreach 标签的 index 值里的内容作为 key,计数器的值 i 作为 value 存储到 bingdings 中。
    // 例如第一次循环就为("index",0)。注意:由于相同的key会被覆盖住,所以最终存储的为("index",userIds.length - 1)
    // 同时生成一个 key 为 ITEM_PREFIX + index 值内容 + "_" + uniqueNumber,value 为 uniqueNumber 存储到 bingdings 中。
    // 例如第一次循环就为("__frch_index_0",0)
    applyIndex(context, i, uniqueNumber);
    
    // 把 foreach 标签的 item 值里的内容作为 key,本次遍历数组中的值作为 value 存储到 bingdings 中。
    // 例如第一次循环就为("userId","1")。注意:由于相同的key会被覆盖住,所以最终存储的为("index",userIds[userIds.length - 1])
    // 同时生成一个 key 为 ITEM_PREFIX + item 值内容 + "_" + uniqueNumber,value 为本次遍历数组中的值存储到 bingdings 中。
    // 例如第一次循环就为("__frch_userId_0","1")
    applyItem(context, o, uniqueNumber);
    
    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
    if (first) {
      first = !((PrefixedContext) context).isPrefixApplied();
    }
    context = oldContext;
    // 计数器加一
    i++;
  }
  // foreach 遍历完,添加 foreach 标签定义的 close 内容
  applyClose(context);
  return true;
}

源码可以知道,问题就出在遍历 dataOrgCodes 这个数组上面。在执行 apply 方法之中有

applyIndex(context, i, uniqueNumber);

applyItem(context, o, uniqueNumber);

#ForEachSqlNode
private void applyIndex(DynamicContext context, Object o, int i) {
  if (index != null) {
    context.bind(index, o);
    context.bind(itemizeItem(index, i), o);
  }
}

private void applyItem(DynamicContext context, Object o, int i) {
  if (item != null) {
    context.bind(item, o);
    context.bind(itemizeItem(item, i), o);
  }
}

#DynamicContext
public void bind(String name, Object value) {
	bindings.put(name, value);
}

从上面的逻辑中可以知道,在遍历 dataOrgCodes 数组的时候,会把我们定义在 foreach 标签中

item、index 属性值作为 key 存储在 DynamicContext 的 bingdings 中,也就是我们传进来的查询参数对象对应的 Map 中,这就导致了虽然我们没有传进来 dataOrgCode 属性,但是在执行 dataOrgCodes 的 foreach 过程中产生了中间值 dataOrgCode,导致最终拼接的 SQL 出现了不该有的条件语句。

五、解决办法

按道理我们使用的框架是 Mybatis 二次开发的(基本是 Mybatis),应该不会有这么大的问题。所以在发现问题后在本地写了一个 demo 进行复现,发现本地的不会出现这个问题,顿时疑惑了。然后就去了 GitHub 把 Mybatis 的源码拉下来进行比较,最终发现了一些问题。

Mybatis 在 2017 年发现了问题并进行了修复,在方法结尾处添加了移除本次 foreach 遍历产生的中间值,也就是从参数映射 Map 中删除了我们定义在 <foreach> 标签的 item、index 定义的 key,这样就不会产生本节的问题。

然而我所用的框架依然是没有更新,用的还是 2012 年版本的代码。所以为了解决这个问题,只能修改 foreach 标签中的 item 的属性值名称,避免和 if 标签的 test 中的属性名称冲突。也就是修改为以下的 SQL 代码。

六、总结

使用二次开发的框架可能存在坑,需要注意引用的版本存在未解决问题。

到此这篇关于Mybatis中SQL节点的文章就介绍到这了,更多相关Mybatis中SQL节点解析内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 关于Mybatis中SQL节点的深入解析

本文链接: https://lsjlt.com/news/143102.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 关于Mybatis中SQL节点的深入解析
    目录一、文章引出原因二、存在的问题三、分析 SQL 生成过程四、分析多余 SQL 的生成五、解决办法六、总结一、文章引出原因 某天在完成项目中的一个小功能后进行自测的时候,发现存在一...
    99+
    2024-04-02
  • Mybatis中SQL节点实例分析
    这篇文章主要讲解了“Mybatis中SQL节点实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Mybatis中SQL节点实例分析”吧!一、文章引出原因某天在完成项目中的一个小功能后进行...
    99+
    2023-06-29
  • 关于Mybatis动态sql中test的坑点总结
    目录总结Mybatis动态sql中test的坑判断相等的注意点判断字符是否相等动态sql标签的小陷阱下面先举个正常的例子总结Mybatis动态sql中test的坑 在mybatis中...
    99+
    2024-04-02
  • 整理几个关键节点深入理解nodejs
    目录前言非阻塞I/Onodejs的非阻塞 I/O事件驱动异步编程回调函数格式规范异步流程控制promisethen & .catchpromise解决异步流程控制async/...
    99+
    2024-04-02
  • 关于mybatis使用${}时sql注入的问题
    目录mybatis使用${}时sql注入的问题区别解决方法mybatis sql注入问题之$与#在mybatis中使用$符号在mybatis中使用#符号mybatis使用${}时sq...
    99+
    2024-04-02
  • 关于对python中self的深入理解
    假设有一个类nameMain(), 如最下面代码 类 : 一个抽象的模板。可以理解为抽象设计图类名:类的名字。查看/实现方式 :print(nameMain)或者print(self...
    99+
    2024-04-02
  • MyBatis深入解读动态SQL的实现
    目录if和wheretrimChooseSetforeachmybatis最强大的功能之一便是它的动态sql能力        借用...
    99+
    2024-04-02
  • 关于JSONP跨域请求原理的深入解析
    目录什么是同源策略什么是JSONP练习jsonp的缺点总结什么是同源策略 同源策略,它是由Netscape提出的一个著名的安全策略。现在所有支持JavaScript 的浏览器都会使用...
    99+
    2024-04-02
  • 如何深入分析Linux系统的inode节点
    这篇文章将为大家详细讲解有关如何深入分析Linux系统的inode节点,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。inode是Linux系统中储存文件信息的区域,也被称为”索引节点”。1 ...
    99+
    2023-06-28
  • 关于Python中*args和**kwargs的深入理解
    目录1. 理解  *  和  **  2.Python函数的参数  3. 支持任意参数的函数 *args, **kwargs 4....
    99+
    2024-04-02
  • 深入理解SQL解析的内涵
    SQL解析:探究其背后的意义,需要具体代码示例引言:SQL(Structured Query Language)是结构化查询语言的缩写,是一种用于管理和操作关系型数据库的标准语言。作为一种强大的数据操作语言,SQL的解析是数据管理和查询的基...
    99+
    2023-12-28
    解析 (Parsing) SQL (Structured Query Language) 背后的意义 (underlyi
  • 深入理解Java中的final关键字_动力节点Java学院整理
    Java中的final关键字非常重要,它可以应用于类、方法以及变量。这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使用final关键字的实例。final经常...
    99+
    2023-05-31
    java final 关键字
  • 关于JDBC与MySQL临时表空间的深入解析
    背景 临时表空间用来管理数据库排序操作以及用于存储临时表、中间排序结果等临时对象,相信大家在开发中经常会遇到相关的需求,下面本文将给大家详细JDBC与MySQL临时表空间的相关内容,分享出来供大家参考学习...
    99+
    2024-04-02
  • 关于MySQL死锁问题的深入分析
    前言 如果我们的业务处在一个非常初级的阶段,并发程度比较低,那么我们可以几年都遇不到一次死锁问题的发生,反之,我们业务的并发程度非常高,那么时不时爆出的死锁问题肯定让我们非常挠头。不过在死锁问题发生时,很多...
    99+
    2024-04-02
  • 关于RestTemplate的使用深度解析
    目录一、概述选择一个优秀的HTTPClient的重要性优秀的HTTPClient需要具备的特性连接池超时时间设置(连接超时、读取超时等)是否支持异步请求和响应的编解码可扩展性答案二、...
    99+
    2024-04-02
  • 关于SQL的cast()函数解析
    注意:本文使用数据库为:mysql5.6 解析: CAST函数用于将某种数据类型的表达式显式转换为另一种数据类型。 CAST()函数的参数是一个表达式,它包括用AS关键字分隔的源值和目标数据类型。 语法: CAST (e...
    99+
    2023-04-28
    SQL cast() cast()函数
  • 深入解析Python中的浮点数输入方法
    Python中浮点型的输入方法详解 在Python编程中,我们经常需要从用户那里获取输入数据。当涉及到浮点型数据时,如何准确地读取和处理用户的输入就变得至关重要。本文将详细介绍Python中浮点型的输入方法,并提供具体代码示例。...
    99+
    2024-02-03
  • MyBatis连接池的深入和动态SQL详解
    目录一,Mybatis 连接池与事务深入1.1 Mybatis 的连接池技术1.1.1 Mybatis 连接池的分类1.1.2 Mybatis 中数据源的配置1.2 Mybatis ...
    99+
    2024-04-02
  • Java Mybatis框架由浅入深全解析中篇
    目录前言添加框架的步骤在idea中添加数据库的可视化添加jdbc.properties属性文件(数据库配置)添加SqlMapCongig.xml创建实体类Student用来封装数据添...
    99+
    2024-04-02
  • 关于Java SE数组的深入理解
    目录1、数组的基本概念1.1 我们为什么需要数组?1.2 数组的创建与初始化1.3 数组的使用1.4 数组的遍历 2、引用类型数组的深入讲解2.1 简单了解 JVM 的内存...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作