我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Mybatis中单双引号引发的惨案及解决

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Mybatis中单双引号引发的惨案及解决

#{}与${}的区别

#{}是预编译处理,${}是字符串替换Mybatis在处理#{}时,会将sql中的#{}替换为?号, 调用PreparedStatement的set方法来赋值;

Mybatis在处理时 , 就 是 把 {}时,就是把时,就是把{}替换成变量的值。

使用#{}可以有效的防止SQL注入,提高系统安全性。

再通俗的说,使用${}mybatis会把参数加上双引号,而${} 你给啥,sql语句中就是啥,如下示例:

select * from table where name = #{name}  name->小明 
## 结果:select * from table where name = "小明"
select * from table where name = ${name}  name->小明 
## 结果:select * from table where name = 小明

问题

最近有个功能需要从sqlserver中去数据,有个脚本很简单如下:

select * from table where id in(...) 

id已经创建索引了,考虑到数据传输,我每次设置的集合大小为100个,因为这是再简单不过的语句了,直接上线给别人使用,但是别人的反馈是,使用50个id需要40多秒!!! 这就有点吓人了,幸好此场景只是在半夜定时的去使用,慢一点不会对第二天有影响,但是白天想要测试的时候就懵了。当然了40多s就别提是否影响别人使用了,基本上就已经崩溃了好不好!!!

这就有点吓人了,幸好此场景只是在半夜定时的去使用,慢一点不会对第二天有影响,但是白天想要测试的时候就懵了。当然了40多s就别提是否影响别人使用了,基本上就已经崩溃了好不好!!!

下面简化了一下,对应的xml代码如下:

<select id="selectTbdIdByLbdIdList" resultType="xxx.xxx.xxMapper">
    SELECT id ,tid FROM table where id IN
    <foreach collection="list" item="item" open="(" close=")" separator=",">
        #{item}
    </foreach>
</select>

debug 模式下的输出如下:

| ==>  Preparing: SELECT id ,tid FROM table where id IN ( ?,?,?,?,?,?...) 
| ==> Parameters: 123(String),234(String),345(String),456(String),
| <==      Total: ....

我把sql整理出来放在sqlserver客户端去执行

SELECT id ,tid FROM table where id IN ( "123","234","345"...);

刚开始执行报错了,后面把双引号改成单引号就行了,即

SELECT id ,tid FROM table where id IN ( '123','234','345'...);
耗时: 0.092s

记住这里的单双引号的问题

??? 很快啊,这是什么情况,第一次遇到这种情况,直接运行sql很快,但是通过mybatis就很慢。

所以我首先怀疑是ORM框架的问题,接着我用JDBC快速写了个demo,来验证,代码如下:

String connectionUrl = "jdbc:sqlserver://xxx:8838;DatabaseName=xxx;user=xxx;password=xx";
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
Connection con = DriverManager.getConnection(connectionUrl);
Statement stmt = con.createStatement();
String SQL = "SELECT id ,tid FROM table where id IN ( '123','234','345'...)";
long s = System.nanoTime();
ResultSet rs = stmt.executeQuery(SQL);
System.out.println((System.nanoTime() - s) / 1_000_000);
// Iterate through the data in the result set and display it.
while (rs.next()) {
    System.out.println(rs.getString("id") + " ---> " + rs.getString("tid"));
}
// 耗时0.109ms

这里也是很快,没什么问题,忽略ORM的问题。

因为我这里用的是Mybatis-Plus,所以我又怀疑是mp的问题,于是debug代码,最后卡在这个地方:

//PreparedStatementHandler.class
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();// 卡在这一行
    return resultSetHandler.handleResultSets(ps);
}

但这是Mybatis的代码,再者说mp只是简化了代码生成这一块,对Mybatis本身的执行没有影响,所以mp也被排除!

这个时候已经过去很长时间了,整个人很懵,怎么会这样???这么简单的sql还会出这么大的问题!我重新理了下思绪,此处的sql是在sqlserver上执行的,那会不会是sqlserver上的问题呢?

我突然灵光一闪,刚刚debug出来的脚本直接放在sqlserver的客户端上执行的时候是有问题的,我后面是把双引号改成单引号才成功的,我赶紧调整了xml中的脚本,如下:

<select id="selectTbdIdByLbdIdList" resultType="xxx.xxx.xxMapper">
    SELECT id ,tid FROM table where id IN
    <foreach collection="list" item="item" open="(" close=")" separator=",">
        '${item}'
    </foreach>
</select>

然后再执行,debug出来的脚本如下:

| ==>  Preparing: SELECT id ,tid FROM table where id IN ( '123','234','345','456'...) 
| ==> Parameters: 
| <==      Total: ....

耗时: 0.100s!!!

如释重负,原来是双引号惹的祸!

SqlServer是不支持双引号的,但是mybatis最后生成的sql使用的双引号,当然这对mysql是没问题的,当然也有例外

如果SQL服务器模式启用了NSI_QUOTES,可以只用单引号引用字符串。用双引号引用的字符串被解释为一个识别符。

所以我遇到的情况是就是生成带双引号的脚本丢给sqlserver执行的时候,被sql服务器误认为是一个识别符,类似java中类型的强转,此时索引是不生效的,也就是说一开的in查询时没有使用到索引的!!!话说那个表中有700w条记录,怪不得每次查询50条的时候,耗时很均匀,都在40多秒。。。。。

回到开头,这种情况就是借助${}来解决,当然是用它是有隐患的,因为它并不能防止sql注入,但是对于我这边的场景不会出现这种情况,所以我赶紧的把其他地方也都改了过来!!!

最后

解决问题还是要大胆假设,小心求证 事实的真相只有一个!!!

另外在debug的时候,顺便看到了#{}和${}的拼接代码,放在了下面

// ForEachSqlNode
public void appendSql(String sql) {
    GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
        String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
        if (itemIndex != null && newContent.equals(content)) {
            newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
        }
        return "#{" + newContent + "}";
    });
    delegate.appendSql(parser.parse(sql));
}
// TextSqlNode  
private GenericTokenParser createParser(TokenHandler handler) {
  return new GenericTokenParser("${", "}", handler);
}
// GenericTokenParser
public String parse(String text) {
    if (text == null || text.isEmpty()) {
        return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
        return text;
    }
    char[] class="lazy" data-src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
        if (start > 0 && class="lazy" data-src[start - 1] == '\\') {
            // this open token is escaped. remove the backslash and continue.
            builder.append(class="lazy" data-src, offset, start - offset - 1).append(openToken);
            offset = start + openToken.length();
        } else {
            // found open token. let's search close token.
            if (expression == null) {
                expression = new StringBuilder();
            } else {
                expression.setLength(0);
            }
            builder.append(class="lazy" data-src, offset, start - offset);
            offset = start + openToken.length();
            int end = text.indexOf(closeToken, offset);
            while (end > -1) {
                if (end > offset && class="lazy" data-src[end - 1] == '\\') {
                    // this close token is escaped. remove the backslash and continue.
                    expression.append(class="lazy" data-src, offset, end - offset - 1).append(closeToken);
                    offset = end + closeToken.length();
                    end = text.indexOf(closeToken, offset);
                } else {
                    expression.append(class="lazy" data-src, offset, end - offset);
                    offset = end + closeToken.length();
                    break;
                }
            }
            if (end == -1) {
                // close token was not found.
                builder.append(class="lazy" data-src, start, class="lazy" data-src.length - start);
                offset = class="lazy" data-src.length;
            } else {
                builder.append(handler.handleToken(expression.toString()));
                offset = end + closeToken.length();
            }
        }
        start = text.indexOf(openToken, offset);
    }
    if (offset < class="lazy" data-src.length) {
        builder.append(class="lazy" data-src, offset, class="lazy" data-src.length - offset);
    }
    return builder.toString();
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Mybatis中单双引号引发的惨案及解决

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

PHP中单引号和双引号的区别详解

看好多代码有时候用单引号或双引号实现包含字符串的内容,其实简单个概括下双引号中的变量可以解析,单引号就是绝对的字符串,下面这篇文章主要给大家介绍了关于PHP中单引号和双引号区别的相关资料,需要的朋友可以参考下
2023-01-15

单引号与双引号在PHP中的差异及影响

单引号与双引号在PHP中的差异及影响在PHP中,单引号(')和双引号(")被用来表示字符串。虽然它们看起来很相似,但在使用时却存在一些差异和影响。本文将详细介绍单引号和双引号在PHP中的差异,并通过具体的代码示例来说明它们的影响。差异:单
单引号与双引号在PHP中的差异及影响
2024-03-05

PHP中单引号与双引号的区别及应用场景

PHP中单引号与双引号是两种用于表示字符串的不同方式,它们在一些方面有一些细微的区别。在本文中,我们将探讨PHP中单引号与双引号的区别以及它们在不同场景下的应用。首先,我们来看看单引号和双引号的基本用法。在PHP中,我们可以使用单引号或双
PHP中单引号与双引号的区别及应用场景
2024-03-05

PHP中单引号和双引号的使用规则解析

在PHP中,单引号和双引号是两种常见的字符串包裹方式,它们在使用时有着不同的特点和规则。本文将分别对单引号和双引号的使用规则进行解析,并提供具体的代码示例来帮助读者更好地理解它们的区别。一、单引号的使用规则:单引号内的内容会被原样输出,不
PHP中单引号和双引号的使用规则解析
2024-03-05

深入理解PHP中单引号和双引号的不同之处

在PHP编程中,单引号和双引号虽然看似相同,但实际上在使用场景和功能上存在着一些细微的差别。深入理解单引号和双引号的不同之处,可以帮助开发者更好地掌握PHP中字符串的处理方式,避免一些不必要的错误。下面将通过具体的代码示例来详细说明单引号和
深入理解PHP中单引号和双引号的不同之处
2024-03-05

golang中的单引号转义问题怎么解决

今天小编给大家分享一下golang中的单引号转义问题怎么解决的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。golang的单引
2023-07-05

nodejs中art-template模板语法的引入及冲突解决方案

使用Webstorm创建nodejs express应用时,默认使用的是jade或者ejs模板,对于不习惯这两种模板语法的人来说确实不是很方便。没关系,这里我们使用art-template模板引擎,使用后可以直接使用html模板: 1、安装
2022-06-04

Python输出列表(List)不带中括号和引号的问题及解决方法

这篇文章主要介绍了Python输出列表(List)不带中括号和引号的问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
2023-02-27

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录