目录

踩坑背景

项目架构:Spring Boot + MyBatis + MySQL。

使用MyBatis作为ORM框架,jdbc驱动使用的是mariadb-java-client

<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.3.0</version>
</dependency>

为了不使用xml形式的配置文件,MyBatis使用接口映射器,并使用映射器注解方式编写SQL语句。

@Mapper
public interface TestDAO {
@Select("select * from test where id = #{id}")
public Test getById(@Param("id") long id);
}

问题描述

在批量添加记录时通过SQLProvider动态拼装SQL,具体代码示例如下所示:

@Repository
@Mapper
public interface TestDAO {
// 使用SQLProvider拼装SQL实现批量插入
@InsertProvider(type = TestProvider.class, method = "addTestBatch")
public Integer addTestBatch(@Param("tests") List<Test> tests);
} public class TestProvider {
public String addTestBatch(List<Test> tests) {
StringBuffer buffer = new StringBuffer().append("insert into scene(id,name,data,thumbnail,comments,ctime,mtime) values");
int size = tests.size();
for(int i = 0; i < size; i++) {
Test test = tests.get(i);
buffer.append("(")
.append(test.getId()).append(",")
.append("'").append(test.getName()).append("'").append(",")
.append("'").append(test.getData()).append("'").append(",")
.append("'").append(test.getThumbnail()).append("'").append(",")
.append("'").append(test.getComments()).append("'").append(",")
.append("now()").append(",")
.append("now()")
.append(")");
if(i < (size - 1)) {
buffer.append(",");
}
}
return buffer.toString();
}
}

Test对象的data属性值为json字符串,其中带有MySQL转意字符“\”,使用上述方式添加记录时会导致test对象的data属性值中的字符“\”被删除掉。

具体来说,假设Test对象的data属性值为:{"value":"{\"x\":277,\"y\":29}"},插入MySQL之后变成了:{"value":"{"x":277,"y":29}"}

显然,Test对象的data属性值插入MySQL之后其中的字符“\”被删除了,这将导致该属性再次从MySQL中查询出来之后无法使用!

原因追踪

一开始我以为是MyBatis的原因导致的,因为使用如下方式插入单调记录是没有问题的:

@Repository
@Mapper
public interface TestDAO {
// 插入单条记录
@Insert("insert into test(id,name,data,thumbnail,comments,ctime,mtime) values(#{id},#{name},#{data},#{thumbnail},#{comments},now(),now())")
public Integer add(Scene scene);
}

通过程序日志可以看到2种方式使用的SQL语句不一样!

通过SQLProvider拼装SQL的方式在日志中看到发送给MySQL的语句为:

而通过@Insert注解方式定义SQL在日志中看到发送给MySQL的语句为:

显然,二者的区别在于:前者使用PreparedStatement时参数列表为空,实际上列值已经在SQL语句中了,本质上并没有使用PreparedStatement。

排查到这里,心里基本有点眉目了,该问题大概率不是MyBatis的锅!

于是我直接把第一种方式的SQL语句通过MySQL客户端执行,果然插入MySQL之后其中的字符“\”被删除了!!!

也就是说,这其实是MySQL本身的原因导致的,最终通过查阅MySQL官方文档得以确认:



上述这段话的大概意思就是说,MySQL在默认情况下(SQL模式不是“NO_BACKSLASH_ESCAPES”)会将插入字段中的字符“\”删除掉。

解决方案

既然找到的问题的根源,那就不难解决了。

实际上,有2种解决办法:

方法一

修改MySQL配置,让MySQL的SQL模式运行在“NO_BACKSLASH_ESCAPES”模式下。

默认情况下,MySQL的SQL模式不包含“NO_BACKSLASH_ESCAPES”。修改配置文件“/etc/my.cnf”,重启MySQL即可。

$ sudo vim /etc/my.cnf
$ [mysqld]
sql-mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,NO_BACKSLASH_ESCAPES

修改之后重启MySQL,再次查看SQL模式:

修改MySQL的SQL模式为“NO_BACKSLASH_ESCAPES”之后,再次插入带有字符“\”的内容就不再会被删除了。

方法二

虽然方法一可以解决问题,但是未免太过于兴师动众,而且对于线上运行的实例通常不能做重启操作。另一个解决办法就是通过在JDBC客户端解决,只要确保在客户端使用PreparedStatement预处理语句即可解决该问题。原因是在PreparedStatement预处理语句中会对转义字符做处理,如下我们通过追踪“mariadb-java-client”的源码来确认一下。

显然,在PreparedStatement预处理语句中会对转义字符做特别处理,具体来讲:当查询的字段中包含'"\NUL时,会在这些字符前面再加一个转义字符\,所以最终发送给MySQL服务器的SQL语句中这些字符对应就变成了\'\"\\\NUL,如果此时MySQL的SQL模式不是”NO_BACKSLASH_ESCAPES“时,会删除其中的转义字符\,这样就可以使得插入到数据库中的这些特殊字符还原为自身了。

到这里我们再来回看方法一的解决方式并不优雅而且笨重,甚至会带来诸多限制。一旦使用了方法一的解决方案,那么就不能在客户端使用预处理语句PreparedStatement了,否则将会导致最终插入到MySQL中的特殊字符多带一个转义字符”\“,将会带来新的问题。

再次回到实际开发中的场景,当使用MyBatis作为ORM框架时,只使用接口映射器的情况下,该如何配置SQL语句才能实现批量插入呢?

实际上,MySQL的映射器注解支持xml风格的动态SQL配置,如下所示:

@Insert({
"<script>",
"insert into test(id,name,data,thumbnail,comments,ctime,mtime) values",
"<foreach item='test' index='index' collection='tests' separator=','>",
"(#{test.id},#{test.name},#{test.data},#{test.thumbnail},#{test.comments},now(),now())",
"</foreach>",
"</script>"
})
public Integer addTestBatch(@Param("tests") List<Test> tests);

使用这种方式的SQL配置,也会使用PreparedStatement预处理方式对特殊字符进行处理,所以可以解决问题。

【参考】

https://fbd.intelleeegooo.cc/mysql-insert-single-quotation-backslash/ mysql语句插入含单引号或者反斜杠的值

https://codeday.me/bug/20180523/170824.html 在mysql中设置全局sql_mode

https://blog.csdn.net/mydriverc2/article/details/79226492 MySQL中如何插入反斜杠,反斜杠被吃掉,反斜杠转义之我见

https://www.cnblogs.com/end/archive/2011/04/01/2002516.html MySql字符转义

https://mybatis.org/mybatis-3/zh/dynamic-sql.html MyBatis动态SQL

https://stackoverflow.com/questions/29803628/how-to-use-foreach-statement-in-selectprovider-class-with-mybatis3 How to use statement in @SelectProvider class with MyBatis3

https://www.cnblogs.com/zhangminghui/p/4903351.html Mybatis之动态构建SQL语句

http://ascii.911cha.com/ ASCII码对照表

MyBatis踩坑之SQLProvider转义字符被删除问题的更多相关文章

  1. 转载:mybatis踩坑之——foreach循环嵌套if判断

    转载自:作者:超人有点忙链接:https://www.jianshu.com/p/1ee41604b5da來源:简书 今天在修改别人的代码bug时,有一个需求是在做导出excel功能时,mybatis ...

  2. MyBatis踩坑记录

    在线文档: 动态SQL  http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html 1. Error setting null for paramete ...

  3. SpringBoot整合mybatis踩坑

    springboot整合mybaits过程中,调用接口时报错:org.apache.ibatis.binding.BindingException: Invalid bound statement ( ...

  4. 初学者手册-MyBatis踩坑记(org.apache.ibatis.binding.BindingException)

    1.参数绑定失败 org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.Bi ...

  5. Java int和integer有什么区别 (mybatis踩坑)

    不要在实体类中使用int 我们都知道Integer是int的包装类,而int是基本数据类型.所以Integer类型的变量会初始化为null,int类型则会被初始化为0 . 所以在下面的动态拼接例子中: ...

  6. k8s删除节点后再重新添加进去(踩坑)

    开启本地集群,发现一台节点出问题了,想删除再换一台节点,结果就踩坑了,还好本地有好几套环境. 再master节点执行以下命令 [root@k8s-master ~]# kubectl drain k8 ...

  7. 一次shardingjdbc踩坑引起的胡思乱想

    项目里面的一个分表用到了sharding-jdbc 当时纠结过是用mycat还是用sharding-jdbc的, 但是最终还是用了sharding-jdbc, 原因如下: 1. mycat比较重, 相 ...

  8. 【踩坑速记】二次依赖?android studio编译运行各种踩坑解决方案,杜绝弯路,总有你想要的~

    这篇博客,只是把自己在开发中经常遇到的打包编译问题以及解决方案给大家稍微分享一下,不求吸睛,但求有用. 1.大家都知道我们常常会遇到dex超出方法数的问题,所以很多人都会采用android.suppo ...

  9. jQuery升级踩坑大全

    jQuery升级踩坑大全 背景 jQuery想必各个web工程师都再熟悉不过了,不过现如今很多网站还采用了很古老的jQuery版本.其实如果早期版本使用不当,可能会有DOMXSS漏洞,非常建议升级到j ...

随机推荐

  1. Maven pom文件中dependency scope用法

    在Maven中依赖的域有:compile.provided.runtime.system.test.import 一.compile(默认) 当依赖的scope为compile的时候,那么当前这个依赖 ...

  2. new String("123") 创建了几个对象?

    String 对象可谓再熟悉不过了,与此相关的面试题经常会引出内存性能优化的问题,本篇主要以 new String("123") 创建了几个对象为例记录. 一.你能回答正确吗 St ...

  3. vim文本编辑器——替换、保存退出

    1.替换: (1)全文替换: 利用查询命令查询: (2)指定替换的字符串的范围: 2.保存.退出命令: (1)在命令行模式下保存(:w) (2)另存为(:w+要保存的文件的路径) (3)保存退出(:w ...

  4. 2.Servlet入门

    一.Servlet简介 Servlet为sun公司开发动态web的一门技术 Sun公司在这些API中提供了一个接口叫做:Servlet,如果想开发Servlet程序,需要完成两个小步骤: 编写一个类, ...

  5. 原生js如何判断元素出现在可视区

    元素出现在可视区 scorll滑动的距离>=当前元素距离浏览器最顶端的高度+当前元素自身的高度-当前可视区的高度 触底 scorll滑动的距离>=当前scroll总高度-当前可视区的高度

  6. 设置Git--在Git中设置您的用户名--创建一个回购--Fork A Repo--社会化

    设置Git GitHub的核心是名为Git的开源版本控制系统(VCS).Git负责计算机上本地发生的所有GitHub相关的事情. 要在命令上使用Git,您需要在计算机上下载,安装和配置Git. 如果要 ...

  7. 利用iptables做端口转发

    需求背景: A与C不在同一网段无法直接访问,而A和B,C和B可以互通.现需要A借助B访问C的3306端口. 解决方案: 利用iptables配置规则,实现端口转发. 具体操作: 在B上开启端口转发功能 ...

  8. 【Beta阶段】第八次Scrum Meeting

    每日任务内容 队员 昨日完成任务 明日要完成的任务 张圆宁 #63 技术博客--django和mysqlhttps://github.com/rRetr0Git/rateMyCourse/issues ...

  9. Gaze Estimation学习笔记(2)-It's Written All Over Your Face Full-Face Appearance-Based Gaze Estimation

    目录 前言 将完整脸部图像作为输入的空间权重CNN方法 将full-face image作为输入的原因 加入空间权重的CNN方法 基础CNN结构 空间权重机制 实验及分析 头部姿态.面部表现视线方向的 ...

  10. 【转】Linux下的CPU使用率与服务器负载的关系与区别

    当我们使用top命令查看系统的资源使用情况时会看到load average,如下图所示,它表示系统在1,5,15分钟的平均工作负载. 那么什么是负载(load)呢?它和CPU的利用率又有什么关系呢? ...