没想到,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
DataTruncationexceptions as is required by the JDBC specification, unless the connection was configured not to do so by using the propertyjdbcCompliantTruncationand 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
没想到,JDBC 驱动会偷偷修改 sql_mode 的会话值的更多相关文章
- 没想到cnblog也有月经贴,其实C#值不值钱不重要。
呵呵,就不倚老卖老了,从basic走过来,一路经历vb,vf,delphi,C#,php,asp,html,js,css,太多太多的开发语言,包括面向对象编程思想,语义化页面结构等等,除了高级的编程技 ...
- MySQL JDBC驱动版本与数据库版本的对应关系及注意事项
MySQL JDBC驱动版本与数据库版本的对应关系及注意事项 事情发生 学了三遍的servlet,经典老师又教的第一万遍登陆注册,并且让实现,并且让演示,我们老师可能和之前的小学期公司老师 完全没有沟 ...
- JDBC驱动自身问题引发的FullGC
公众号HelloJava刊出一篇<MySQL Statement cancellation timer 故障排查分享>,作者的某服务的线上机器报 502(502是 nginx 做后端健康检 ...
- 关于iBatis.NET连接各数据库时提示没找到数据库驱动的依赖文件
iBatis.net在连接oracle数据库时使用的是:oracleClient1.0 这个是系统自带的驱动,配置上即可,使用的连接配置为: <database> <provider ...
- Oracle、DB2、MySql、SQLServer JDBC驱动
四种数据库JDBC驱动,还列出了连接的Class驱动名和Url Pattern,DB2包括Type 2.Type 3和Type 4三种模式.注意驱动包名称的大小写. Oralce连接驱动包名和URL ...
- Oracle Jdbc驱动下载及安装本地maven仓库
由于二进制许可 binary license的限制,oracle jdbc驱动不能通过共有仓库来获取,所以你可以下载下来添加到自己的本地仓库或私有仓库中. 添加到本地仓库步骤如下: 下载Oracle ...
- centos clamav杀毒软件安装配置及查杀,没想到linux下病毒比windows还多!
centos clamav杀毒软件安装配置及查杀,没想到linux下病毒比windows还多! 一.手动安装 1.下载(官网) cd /soft wget http://www.clam ...
- 关于mysql-connector-java(JDBC驱动)的一些坑
最近在写一个项目的时候,用了maven仓库里面较新的mysql的JDBC驱动,版本是6.0.6,Mybatis的全局配置是这么写的: <?xml version='1.0' encoding=' ...
- java加载jdbc驱动三种方式的比较
一.引言 平时连接数据库的时候首先要加载jdbc驱动,这一步骤其实有三种方式,他们的区别?优劣? 二.快速了解三种加载方式 Class.forName(“com.mysql.jdbc.Driver”) ...
- [Java] Oracle的JDBC驱动的版本说明
classes12.jar,ojdbc14.jar,ojdbc5.jar和ojdbc6.jar的区别,之间的差异 作者:赵磊 博客:http://elf8848.iteye.com 来源:http:/ ...
随机推荐
- git中 commit 和 pull 的先后顺序问题会产生多余的merge记录
commit 和 pull 的先后顺序问题 最近提交代码,发现一个问题. 自己很清楚的记得本次的提交是没有进行合并的. 奇怪的死 gitlab中的 history 历史中显示了我对本次进行了Merge ...
- vue中v-model修饰符的使用和组件使用v-model
1.lazy 修饰器 lazy修饰器在input框中的表现效果是: 当你失去焦点后值才会跟新. 它的跟新时机是失去焦点后 这个修饰器在项目中运用的场景较少 <template> <d ...
- P9779_[HUSTFC 2023] 不定项选择题_题解
rt 题目 有一道共 n 个选项的不定项选择题,它的答案至少包含一个选项,由于题目与选项的内容晦涩难懂,你打算通过尝试每一种可能的答案来通过这道题. 初始时所有选项都没有被勾选,你可以执行任意次下述操 ...
- 文字溢出hover展示
我这个后端返回的是html结构,不然不用加v-html,需要依赖element Ui 的文字提示 <el-tooltip placement="top"> <p ...
- python快速入门【二】----常见的数据结构
python入门合集: python快速入门[一]-----基础语法 python快速入门[二]----常见的数据结构 python快速入门[三]-----For 循环.While 循环 python ...
- PaddleNLP通用信息抽取技术UIE【一】产业应用实例:信息抽取{实体关系抽取、中文分词、精准实体标。情感分析等}、文本纠错、问答系统、闲聊机器人、定制训练
相关文章: 1.快递单中抽取关键信息[一]----基于BiGRU+CR+预训练的词向量优化 2.快递单信息抽取[二]基于ERNIE1.0至ErnieGram + CRF预训练模型 3.快递单信息抽取[ ...
- 【3】jupyter notebook【修改主题、字体、字号、代码自动补全、变更工作目录、添加目录插件、解决工具栏隐藏问题、修改默认浏览器、默认打开位置】
相关文章: [1]Anaconda安装超简洁教程,瞬间学会! [2]Anaconda下:ipython文件的打开方式,Jupyter Notebook中运行.py文件,快速打开ipython文件的方法 ...
- .net 工具箱不可用/怎样初始化vs环境 解决方案
在开始菜单里面执行的.开始菜单->Microsoft Visual Studio 2005->Visual Studio Tools->Visual Studio 2005 命令提示 ...
- 【树】N叉树的遍历【力扣589、力扣590】超详细的解释和注释
说在前面 欢迎朋友们来到我的博客. 今天我们的重点是,N叉树的遍历. 今天,博主就带来两道经典的题目,领着大家理解N叉树的前序遍历和后序遍历! 当然,还想学习其它算法的朋友们,可以通过订阅博主的算法专 ...
- 看New Bing回答世纪难题:女友和妈妈掉水里先救谁
1.女友和妈妈掉水里先救谁 今天好奇想看看New Bing怎么回答这种世纪难题 结果New Bing非常聪明,反手建议我不要直接回答这个问题,而是换个角度哄女朋友,带着点不甘心,我继续追问它 New ...