最近碰到一个 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 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 propertyjdbcCompliantTruncation
and setting it tofalse
.