MyBatis踩坑之SQLProvider轉義字符被刪除問題


MyBatis

目錄

踩坑背景

項目架構: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的語句為:
MyBatis通過SQLProvider拼裝SQL

而通過@Insert注解方式定義SQL在日志中看到發送給MySQL的語句為:
MyBatis使用映射器注解定義SQL

顯然,二者的區別在於:前者使用PreparedStatement時參數列表為空,實際上列值已經在SQL語句中了,本質上並沒有使用PreparedStatement。
排查到這里,心里基本有點眉目了,該問題大概率不是MyBatis的鍋!

於是我直接把第一種方式的SQL語句通過MySQL客戶端執行,果然插入MySQL之后其中的字符“\”被刪除了!!!
也就是說,這其實是MySQL本身的原因導致的,最終通過查閱MySQL官方文檔得以確認:
MySQL在插入數據時默認情況下會刪除字段中的轉義字符
上述這段話的大概意思就是說,MySQL在默認情況下(SQL模式不是“NO_BACKSLASH_ESCAPES”)會將插入字段中的字符“\”刪除掉。

解決方案

既然找到的問題的根源,那就不難解決了。
實際上,有2種解決辦法:

方法一

修改MySQL配置,讓MySQL的SQL模式運行在“NO_BACKSLASH_ESCAPES”模式下。
MySQL默認的SQL模式

默認情況下,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模式

修改MySQL的SQL模式為“NO_BACKSLASH_ESCAPES”之后,再次插入帶有字符“\”的內容就不再會被刪除了。

方法二

雖然方法一可以解決問題,但是未免太過於興師動眾,而且對於線上運行的實例通常不能做重啟操作。另一個解決辦法就是通過在JDBC客戶端解決,只要確保在客戶端使用PreparedStatement預處理語句即可解決該問題。原因是在PreparedStatement預處理語句中會對轉義字符做處理,如下我們通過追蹤“mariadb-java-client”的源碼來確認一下。
在PreparedStatment中對轉義字符做處理

顯然,在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碼對照表


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM