最近碰到一个 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 propertyjdbcCompliantTruncation
and setting it tofalse
.
参考资料
- https://docs.oracle.com/cd/E17952_01/connector-j-8.0-en/connector-j-reference-type-conversions.html
- https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-session.html
1.本站内容仅供参考,不作为任何法律依据。用户在使用本站内容时,应自行判断其真实性、准确性和完整性,并承担相应风险。
2.本站部分内容来源于互联网,仅用于交流学习研究知识,若侵犯了您的合法权益,请及时邮件或站内私信与本站联系,我们将尽快予以处理。
3.本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
4.根据《计算机软件保护条例》第十七条规定“为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。”您需知晓本站所有内容资源均来源于网络,仅供用户交流学习与研究使用,版权归属原版权方所有,版权争议与本站无关,用户本人下载后不能用作商业或非法用途,需在24个小时之内从您的电脑中彻底删除上述内容,否则后果均由用户承担责任;如果您访问和下载此文件,表示您同意只将此文件用于参考、学习而非其他用途,否则一切后果请您自行承担,如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
5.本站是非经营性个人站点,所有软件信息均来自网络,所有资源仅供学习参考研究目的,并不贩卖软件,不存在任何商业目的及用途
暂无评论内容