目錄
踩坑背景
項目架構: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
https://www.cnblogs.com/zhangminghui/p/4903351.html Mybatis之動態構建SQL語句
http://ascii.911cha.com/ ASCII碼對照表