最近碰到一个 case,值得分享一下。

现象就是一个 update 操作,在 mysql 客户端中执行提示 warning,但在 java 程序中执行却又报错。

问题重现

mysql> create table test.t1(id int primary key, c1 datetime);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test.t1 values(1,now());
Query OK, 1 row affected (0.00 sec)

mysql> update test.t1 set c1=str_to_date('2024-02-23 01:01:01.0','%Y-%m-%d %H:%i:%s') where id=1;
Query OK, 1 row affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 1

mysql> show warnings;
+---------+------+-------------------------------------------------------------+
| Level   | Code | Message                                                     |
+---------+------+-------------------------------------------------------------+
| Warning | 1292 | Truncated incorrect datetime value: '2024-02-23 01:01:01.0' |
+---------+------+-------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select * from test.t1;
+----+---------------------+
| id | c1                  |
+----+---------------------+
|  1 | 2024-02-23 01:01:01 |
+----+---------------------+
1 row in set (0.00 sec)

update 语句中使用STR_TO_DATE函数将字符串转换为日期时间格式。

但因为这个格式字符串'%Y-%m-%d %H:%i:%s'没有对日期字符串中的毫秒部分.0进行解析,所以这一部分会被 truncate 掉。

可以看到,该语句在 mysql 客户端中执行时没有报错,只是提示 warning。

同样的 SQL,在下面这段 java 代码中跑却直接报错。

package com.example;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcTest {

    private static final String JDBC_URL = "jdbc:mysql://10.0.0.198:3306/information_schema";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection(JDBC_URL, USER, PASSWORD)) {
            try (Statement statement = connection.createStatement()) {
                String updateQuery = "UPDATE test.t1 SET c1 = STR_TO_DATE('2024-02-23 01:01:01.0', '%Y-%m-%d %H:%i:%s') WHERE id=1";
                int rowsAffected = statement.executeUpdate(updateQuery);
                System.out.println("Rows affected: " + rowsAffected);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
# java -jar target/jdbc-test-1.0-SNAPSHOT-jar-with-dependencies.jar
com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Truncated incorrect datetime value: '2024-02-23 01:01:01.0'
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:104)
        at com.mysql.cj.jdbc.StatementImpl.executeUpdateInternal(StatementImpl.java:1337)
        at com.mysql.cj.jdbc.StatementImpl.executeLargeUpdate(StatementImpl.java:2112)
        at com.mysql.cj.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1247)
        at com.example.JdbcTest.main(JdbcTest.java:17)

问题根因

刚开始以为这个报错跟 sql_mode 有关,但实际上这个实例的 sql_mode 为空。

mysql> show global variables like '%sql_mode%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sql_mode      |       |
+---------------+-------+
1 row in set (0.00 sec)

所以,一开始就排除了 sql_mode 的可能性。

但万万没想到,JDBC 驱动会偷偷修改 sql_mode 的会话值。

在上面的 java 程序中加了一段代码,用来打印 sql_mode 的会话值。

ResultSet resultSet = statement.executeQuery("SELECT @@SESSION.sql_mode");
if (resultSet.next()) {
    String sqlModeValue = resultSet.getString(1);
    System.out.println("Current sql_mode value: " + sqlModeValue);
}

结果发现当前会话的 sql_mode 竟然是STRICT_TRANS_TABLES

Current sql_mode value: STRICT_TRANS_TABLES

STRICT_TRANS_TABLES就是导致 update 操作报错的罪魁祸首!

这一点,很容易在 mysql 客户端中验证出来。

mysql> set session sql_mode='STRICT_TRANS_TABLES';
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> update test.t1 set c1=str_to_date('2024-02-23 01:01:01.0','%Y-%m-%d %H:%i:%s') where id=1;
ERROR 1292 (22007): Truncated incorrect datetime value: '2024-02-23 01:01:01.0'

所以,问题来了, sql_mode 是在哪里修改的?

sql_mode 是在哪里修改的?

分析 JDBC 驱动代码,发现会话的 sql_mode 是在setupServerForTruncationChecks中修改的。

该方法是在连接建立后,初始化时调用的。

其主要作用是检查当前会话的 sql_mode 是否包含STRICT_TRANS_TABLES,如果不包含,则会通过 SET 命令修改当前会话的 sql_mode,使其包含STRICT_TRANS_TABLES

// src/main/user-impl/java/com/mysql/cj/jdbc/ConnectionImpl.java
private void setupServerForTruncationChecks() throws SQLException {
    synchronized (getConnectionMutex()) {
        // 获取 JDBC 驱动程序配置中的 jdbcCompliantTruncation 属性
        RuntimeProperty<Boolean> jdbcCompliantTruncation = this.propertySet.getProperty(PropertyKey.jdbcCompliantTruncation);
        if (jdbcCompliantTruncation.getValue()) {
            // 获取当前会话的 sql_mode
            String currentSqlMode = this.session.getServerSession().getServerVariable("sql_mode");
            // 检查 sql_mode 中是否包含 STRICT_TRANS_TABLES 选项
            boolean strictTransTablesIsSet = StringUtils.indexOfIgnoreCase(currentSqlMode, "STRICT_TRANS_TABLES") != -1;
            // 如果 sql_mode 为空,或长度为 0,或不包含 STRICT_TRANS_TABLES 选项,
            // 则构建 SET sql_mode 语句,将 STRICT_TRANS_TABLES 添加到 sql_mode 中
            if (currentSqlMode == null || currentSqlMode.length() == 0 || !strictTransTablesIsSet) {
                StringBuilder commandBuf = new StringBuilder("SET sql_mode='");

                if (currentSqlMode != null && currentSqlMode.length() > 0) {
                    commandBuf.append(currentSqlMode);
                    commandBuf.append(",");
                }
     
                commandBuf.append("STRICT_TRANS_TABLES'");
                // 执行 SET sql_mode 语句
                this.session.execSQL(null, commandBuf.toString(), -1, null, false, this.nullStatementResultSetFactory, null, false);

                jdbcCompliantTruncation.setValue(false); // server's handling this for us now
            } else if (strictTransTablesIsSet) {
                // 如果 sql_mode 中包含 STRICT_TRANS_TABLES 选项,则不做任何调整
                // We didn't set it, but someone did, so we piggy back on it
                jdbcCompliantTruncation.setValue(false); // server's handling this for us now
            }
        }
    }
}

所以,尽管 mysql 服务端的 sql_mode 为空,但由于 JDBC 驱动将会话的 sql_mode 调整为了STRICT_TRANS_TABLES,最后还是导致 update 操作报错。

如何解决 java 程序中执行报错的问题

很简单,在 JDBC URL 中将jdbcCompliantTruncation属性设置为 false。

jdbc:mysql://10.0.0.198:3306/information_schema?jdbcCompliantTruncation=false

除此之外,也可修改 java 代码,在 update 操作之前显式设置 sql_mode 的会话值,如,

statement.execute("SET @@SESSION.sql_mode = ''");
String updateQuery = "UPDATE test.t1 SET c1 = STR_TO_DATE('2024-02-23 01:01:01.0', '%Y-%m-%d %H:%i:%s') WHERE id=1";

但这种方式对应用代码有侵入,不建议这么做。

实际上,JDBC 驱动支持在 URL 中修改参数的会话值。

在 URL 中修改参数的会话值,有以下好处:

  • 无需在每次 SQL 操作之前显式执行设置语句。这使得配置变更更为集中化,更容易管理和维护。

  • 避免了对应用代码的直接侵入,提高了代码的可维护性和灵活性。

JDBC 驱动中如何修改参数的会话值

从 mysql-connector-java 3.1.8 开始,支持通过sessionVariables属性修改 MySQL 参数的会话值。语法如下:

sessionVariables=variable_name1=variable_value1,variable_name1=variable_value2...variable_nameN=variable_valueN

多个参数之间使用逗号或者分号隔开。

看下面这个示例,同时修改 explicit_defaults_for_timestamp,group_concat_max_len 和 sql_mode 的会话值。

JDBC_URL = "jdbc:mysql://10.0.0.198:3306/information_schema?sessionVariables=explicit_defaults_for_timestamp=OFF,group_concat_max_len=2048,sql_mode='NO_ZERO_IN_DATE,NO_ZERO_DATE'"

注意,如果jdbcCompliantTruncation为 true(默认值),即使sessionVariables中设置的 sql_mode 不包含STRICT_TRANS_TABLES,最终生效的 sql_mode 的会话值还是会包含STRICT_TRANS_TABLES

之所以会这样,主要是因为sessionVariables的设置先于setupServerForTruncationChecks

JDBC 驱动为什么要修改 sql_mode 的会话值

这个实际上是 JDBC 规范的要求。

Connector/J issues warnings or throws DataTruncation exceptions as is required by the JDBC specification, unless the connection was configured not to do so by using the property jdbcCompliantTruncation and setting it to false.

参考资料

  1. https://docs.oracle.com/cd/E17952_01/connector-j-8.0-en/connector-j-reference-type-conversions.html
  2. https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-session.html

没想到,JDBC 驱动会偷偷修改 sql_mode 的会话值的更多相关文章

  1. 没想到cnblog也有月经贴,其实C#值不值钱不重要。

    呵呵,就不倚老卖老了,从basic走过来,一路经历vb,vf,delphi,C#,php,asp,html,js,css,太多太多的开发语言,包括面向对象编程思想,语义化页面结构等等,除了高级的编程技 ...

  2. MySQL JDBC驱动版本与数据库版本的对应关系及注意事项

    MySQL JDBC驱动版本与数据库版本的对应关系及注意事项 事情发生 学了三遍的servlet,经典老师又教的第一万遍登陆注册,并且让实现,并且让演示,我们老师可能和之前的小学期公司老师 完全没有沟 ...

  3. JDBC驱动自身问题引发的FullGC

    公众号HelloJava刊出一篇<MySQL Statement cancellation timer 故障排查分享>,作者的某服务的线上机器报 502(502是 nginx 做后端健康检 ...

  4. 关于iBatis.NET连接各数据库时提示没找到数据库驱动的依赖文件

    iBatis.net在连接oracle数据库时使用的是:oracleClient1.0 这个是系统自带的驱动,配置上即可,使用的连接配置为: <database> <provider ...

  5. Oracle、DB2、MySql、SQLServer JDBC驱动

    四种数据库JDBC驱动,还列出了连接的Class驱动名和Url Pattern,DB2包括Type 2.Type 3和Type 4三种模式.注意驱动包名称的大小写. Oralce连接驱动包名和URL ...

  6. Oracle Jdbc驱动下载及安装本地maven仓库

    由于二进制许可 binary license的限制,oracle jdbc驱动不能通过共有仓库来获取,所以你可以下载下来添加到自己的本地仓库或私有仓库中. 添加到本地仓库步骤如下: 下载Oracle ...

  7. centos clamav杀毒软件安装配置及查杀,没想到linux下病毒比windows还多!

    centos clamav杀毒软件安装配置及查杀,没想到linux下病毒比windows还多! 一.手动安装 1.下载(官网)    cd /soft     wget http://www.clam ...

  8. 关于mysql-connector-java(JDBC驱动)的一些坑

    最近在写一个项目的时候,用了maven仓库里面较新的mysql的JDBC驱动,版本是6.0.6,Mybatis的全局配置是这么写的: <?xml version='1.0' encoding=' ...

  9. java加载jdbc驱动三种方式的比较

    一.引言 平时连接数据库的时候首先要加载jdbc驱动,这一步骤其实有三种方式,他们的区别?优劣? 二.快速了解三种加载方式 Class.forName(“com.mysql.jdbc.Driver”) ...

  10. [Java] Oracle的JDBC驱动的版本说明

    classes12.jar,ojdbc14.jar,ojdbc5.jar和ojdbc6.jar的区别,之间的差异 作者:赵磊 博客:http://elf8848.iteye.com 来源:http:/ ...

随机推荐

  1. 【一个小发现】VictoriaMetrics 中 vmselect 的 `-search.denyPartialResponse` 选项不应该开启

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 一直以为vmselect 的 -search.denyPa ...

  2. 在Protocol Buffers中导入当前目录中的.proto文件

    在protobuf中导入当前目录中的.proto文件时,可以使用相对路径.相对路径是相对于当前.proto文件所在的目录来引用其他.proto文件. 假设有以下目录结构: my_project/ |- ...

  3. TienChin-课程管理-展示课程列表

    配置按钮权限 博主这里就不贴SQL了,自行手动添加一下吧. 更改表结构 ALTER TABLE `tienchin_course` MODIFY COLUMN `info` varchar(255) ...

  4. 【深度学习项目五】:利用LSTM网络进行情感分析(NLP)

    相关文章: [深度学习项目一]全连接神经网络实现mnist数字识别](https://blog.csdn.net/sinat_39620217/article/details/116749255?sp ...

  5. C/C++ 使用API实现数据压缩与解压缩

    在Windows编程中,经常会遇到需要对数据进行压缩和解压缩的情况,数据压缩是一种常见的优化手段,能够减小数据的存储空间并提高传输效率.Windows提供了这些API函数,本文将深入探讨使用Windo ...

  6. C/C++ 反汇编:函数与结构体

    反汇编即把目标二进制机器码转为汇编代码的过程,该技术常用于软件破解.外挂技术.病毒分析.逆向工程.软件汉化等领域,学习和理解反汇编对软件调试.系统漏洞挖掘.内核原理及理解高级语言代码都有相当大的帮助, ...

  7. h5st 4.3版本代码研究

    背景介绍 最近比较悠闲,于是没事研究了一下某东的h5st代码,2024年新鲜出炉的前端加密代码: 最大的惊喜并不是算法的复杂,在逆向破解代码的过程中,对js加密混淆有了新的认识: 于是心血来潮,回到这 ...

  8. 记录一则ADG备库报错ORA-29771的案例

    有客户找到我这边咨询,说他们的一套核心ADG库在业务高峰期报错,因为业务做了读写分离,其备库也实际承担读业务,所以备库故障也会对业务产生影响. 这里也要提醒大家,做读写分离,如果读库出现故障的情况,要 ...

  9. SESSION会话机制解析

    Windows Session(会话)的概念 会话 session 是由代表单个用户登录会话的所有进程和系统对象组成的.其中的对象包括所有的窗口,桌面和windows stations.桌面是特定se ...

  10. UVA12655 Trucks 题解

    题目传送门 前言 中文题目可以看 link . 前置知识 Kruskal 重构树 | 最近公共祖先 简化题意 给定一个 \(N\) 个点 \(M\) 条边的有向图,共有 \(S\) 次询问,每次询问从 ...