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[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
        if (start > 0 && src[start - 1] == '\\') {
            // this open token is escaped. remove the backslash and continue.
            builder.append(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(src, offset, start - offset);
            offset = start + openToken.length();
            int end = text.indexOf(closeToken, offset);
            while (end > -1) {
                if (end > offset && src[end - 1] == '\\') {
                    // this close token is escaped. remove the backslash and continue.
                    expression.append(src, offset, end - offset - 1).append(closeToken);
                    offset = end + closeToken.length();
                    end = text.indexOf(closeToken, offset);
                } else {
                    expression.append(src, offset, end - offset);
                    offset = end + closeToken.length();
                    break;
                }
            }
            if (end == -1) {
                // close token was not found.
                builder.append(src, start, src.length - start);
                offset = src.length;
            } else {
                builder.append(handler.handleToken(expression.toString()));
                offset = end + closeToken.length();
            }
        }
        start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
        builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}

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

(0)

相关推荐

  • mybatis 对于生成的sql语句 自动加上单引号的情况详解

    目录 对于生成的sql语句 自动加上单引号的情况 mySQL中replace的用法 mybatis中IFNULL(P1,P2)函数的用法 mybatis单引号字母逻辑处理的一个坑 原因分析 对于生成的sql语句 自动加上单引号的情况 mybatis是这样的,如果表的字段跟系统字段冲突,写sql语句的时候必须得加上单引号,这样才会区分 mySQL中replace的用法 1.replace into replace into table (id,name) values('1','aa'),('2'

  • 基于mybatis中test条件中单引号双引号的问题

    目录 test条件中单引号双引号问题 具体原因 动态sql中test的一些问题 mybatis动态sql中OGNL中type=="1"和type='1'的区别 解决方案 test条件中单引号双引号问题 在mybatis中test判断条件中使用单引号会报错 通常使用双引号 通常test后的判断条件写在双引号内,但是当条件中判断使用字符串时应该如下方式开发 <when  test="channel ==null" > <when  test='chan

  • 解决mybatis #{}无法自动添加引号的错误

    目录 mybatis #{}无法自动添加引号 解决 mybatis #{}与${} 单引号 解决办法 验证 mybatis #{}无法自动添加引号 传入string类型时,无法自动添加引号,导致SQL将值识别为列名,导致SQL失败 解决 使用map类型代替string的传值 如 Map<String, String> map = new HashMap<>(2); map.put("userName", userName); return userMapper.

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

    目录 #{}与${}的区别 问题 最后 #{}与${}的区别 #{}是预编译处理,${}是字符串替换Mybatis在处理#{}时,会将sql中的#{}替换为?号, 调用PreparedStatement的set方法来赋值: Mybatis在处理时 , 就 是 把 {}时,就是把时,就是把{}替换成变量的值. 使用#{}可以有效的防止SQL注入,提高系统安全性. 再通俗的说,使用${}mybatis会把参数加上双引号,而${} 你给啥,sql语句中就是啥,如下示例: select * from t

  • 再谈PHP中单双引号的区别详解

    在PHP中,字符串的定义可以使用英文单引号' ',也可以使用英文双引号" ". 但是必须使用同一种单或双引号来定义字符串,如:'Hello World"和"Hello World'为非法的字符串定义. 单引号和双引号到底有啥区别呢?下面通过本文学习一下吧. 1.定义字符串 在PHP中,字符串的定义可以使用单引号,也可以使用双引号.但是必须使用同一种单或双引号来定义字符串,如:'Hello"和"Hello'为非法的字符串定义. 定义字符串时,只有一

  • C#解析json字符串总是多出双引号的原因分析及解决办法

    json好久没用了,今天在用到json的时候,发现对字符串做解析的时候总是多出双引号. 代码如下: string jsonText = "{'name':'test','phone':'18888888888'}"; JObject jo = (JObject)JsonConvert.DeserializeObject(jsonText); string zone = jo["name"].ToString(); string zone_en = jo["

  • 分析PHP中单双引号的误区和双引号小隐患

    许多程序员以为在PHP中单引号和双引号是一样的,其实这要看怎么用法,在有些方面它们确实是一样,但有一些方面它们也有着很大的区别,今天小编就来为您说说有哪些区别. 1.一般情况下两者是通用的.但如果双引号内写的是变量就会执行解析操作,而单引号则不解析,这个怎么说?还是举个例子吧. 这下看明白了吧! 2.执行效率不一样,单引号的执行速度要比双引号的执行速度快,如果是一样大型的程序,这方面还是要注意优化的,毕竟PHP属于解释型语言.所以如果内部只有纯字符串的时候,用单引号(速度快),内部有别的东西(如

  • 浅谈PHP中单引号和双引号到底有啥区别呢?

    在PHP中,字符串的定义可以使用英文单引号' ',也可以使用英文双引号" ". 但是必须使用同一种单或双引号来定义字符串,如:'Hello World"和"Hello World'为非法的字符串定义. 单引号和双引号到底有啥区别呢? PHP允许我们在双引号串中直接包含字串变量. 而单引号串中的内容总被认为是普通字符,因此单引号中的内容不会被转义效率更高. 比如: 复制代码 代码如下: $str='hello'; echo "str is $str"

  • 我遇到的参数传递中 双引号单引号嵌套问题

    最近学vml::cakepie.innerHTML="<v:shape id='cake"+(i+1)+"'type='#Cake_3D'"+                            " style='position:absolute;left:"+(_left + Height / 8)+"px;top:"+(_top + Height / 24)+"px;WIDTH:"+Heigh

  • Shell脚本中单引号(‘)和双引号(“)的使用区别

    在Linux操作系统上编写Shell脚本时候,我们是在变量的前面使用$符号来获取该变量的值,通常在脚本中使用"$param"这种带双引号的格式,但也有出现使用'$param'这种带引号的使用的场景,首先大家看一段例子: 复制代码 代码如下: [root@linux ~]# name=TekTea [root@linux ~]# echo $name TekTea [root@linux ~]# sayhello="Hello $name" [root@linux ~

  • linux shell中单引号、双引号、反引号、反斜杠的区别

    1. 单引号 ( '' ) # grep Susan phonebook Susan Goldberg 403-212-4921 Susan Topple 212-234-2343 如果我们想查找的是Susan Goldberg,不能直接使用grep Susan Goldberg phonebook命令,grep会把Goldberg和phonebook当作需要搜索的文件 # grep 'Susan Gold' phonebook Susan Goldberg 403-212-4921 当shel

  • PHP中单引号与双引号的区别分析

    ①转义的字符不同 单引号和双引号中都可以使用转义字符(\),但只能转义在单引号中引起来的单引号和转义转义符本身.如果用双引号("")括起字符串,PHP懂得更多特殊字符串的转义序列. <?php $str1 = '\',\\,\r\n\t\v\$\"'; echo $str1,'<br />'; $str2 = "\",\\,a\r\n\tb\v\$\'"; echo $str2,'<br />'; ?> ②对变

随机推荐