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

最近碰到一个 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
相关推荐
zklgin3 分钟前
【MySQL】深度学习数据库开发技术:使用CC++语言访问数据库
数据库·mysql·数据库开发
未秃头的程序猿25 分钟前
深入理解MySQL三大日志:binlog、redo log、undo log,一文彻底搞懂!
mysql
七七powerful42 分钟前
养龙虾--codebuddy调用mysql-mcp-server 查询MySQL
服务器·数据库·mysql·mcp
熙胤1 小时前
【MySQL】数据库和表的操作
数据库·mysql·oracle
jolimark1 小时前
【mysql部署】在ubuntu22.04上安装和配置mysql教程
android·mysql·adb
Y001112362 小时前
Day5-MySQL-SQL-4
数据库·sql·mysql
泯仲2 小时前
从零起步学习MySQL第十三章:MySQL 事务详解:原理、特性、并发问题与隔离级别
数据库·学习·mysql
原来是猿2 小时前
MySQL【基本查询下 - 表的增删改查】
数据库·mysql
..过云雨2 小时前
【负载均衡oj项目】02. comm公共文件夹设计 - 包含所有需要用到的自定义工具
数据库·c++·mysql·html·负载均衡
楼田莉子2 小时前
MySQL数据库的操作
数据库·mysql