首页 > 数据库 >没想到,JDBC 驱动会偷偷修改 sql_mode 的会话值

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

时间:2024-03-04 09:23:52浏览次数:19  
标签:01 java 会话值 sql JDBC mode mysql TRANS

最近碰到一个 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

标签:01,java,会话值,sql,JDBC,mode,mysql,TRANS
From: https://www.cnblogs.com/ivictor/p/18051155

相关文章

  • Towards Foundation Models for Knowledge Graph Reasoning
    目录概符号说明ULTRA(amethodforUnified,Learnable,andTRAnsferableKGrepresentations)RelationGraphConstructionConditionalRelationRepresentations代码GalkinM.,YuanX.,MostafaH.,TangJ.andZhuZ.Towardsfoundationmodelsforknowledgegraphrea......
  • 理解大模型中的 d_model
    在深度学习和Transformer模型的上下文中,d_model中的“d”通常代表“dimension”,即“维度”的简写。因此,d_model指的是模型中向量的维度大小,这是一个关键的参数,影响着模型的性能和计算复杂度。在Transformer架构中,d_model特别指向嵌入向量的维度,以及模型内部传递的数据向量的统一维......
  • C++填坑系列——EffectiveModernC++之类型推导
    接下来会记录我在学习《EffectiveModernC++》的一些总结和思考。鉴于C++的知识太多了,我难以全面覆盖到学习,所以这里借此来补充和发散自己的学习心得:)以下内容由学习这个网站EffectiveModernC++的中文翻译内容得来https://cntransgroup.github.io/EffectiveModernCppChine......
  • C++填坑系列——EffectiveModernC++之新手怎么进入现代C++
    新手怎么进入现代C++1.使用auto来自动推导变量类型2.使用{}来创建变量和对象3.使用nullptr来创建空指针4.使用using代替typedef进行别名定义5.使用enumclass代替enum进行枚举定义6.使用=delete来禁止调用一个函数7.使用override来修饰继承链中的重写函数8.使用co......
  • C++填坑系列——EffectiveModernC++之特殊成员函数
    Chapter移步现代c++之特殊成员函数Item17:Understandspecialmemberfunctiongeneration总结:有必要了解各个函数什么时候自动生成;自动生成的函数有可能产生预期外的行为;特殊成员函数(编译器自动生成):默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋......
  • R语言建立和可视化混合效应模型mixed effect model|附代码数据
    全文下载链接:http://tecdat.cn/?p=20631最近我们被客户要求撰写关于混合效应模型的研究报告,包括一些图形和统计输出我们已经学习了如何处理混合效应模型。本文的重点是如何建立和_可视化_ 混合效应模型的结果设置本文使用数据集,用于探索草食动物种群对珊瑚覆盖的影响。 ......
  • mysql8的sql_mode不起作用
    如果sql_mode似乎没有起作用,可能是由以下几个原因造成的:配置文件位置不正确:确保my.ini文件位于MySQL服务器实际读取配置的位置。对于Windows系统,这通常是MySQL安装目录下的bin文件夹。对于Linux系统,它可能位于/etc/mysql/、/etc/my.cnf或~/.my.cnf等位置。MySQL服务未重启:对......
  • yolo7检测学习Bubbliiiing的视频有感——(1)no model named 'cv2'
    我一直以来也是喜欢用vscode,就跟随B导安装了vscode下载完B导提供的代码后如果想直接跟着视频B导的操作运行predict.py文件是不可能的,需要按照readme文档中的所有提示按照步骤放入所有文件首先针对nomodelnamedcv2这个错误我按照网上的教程先在terminal窗口查询自己是否安装......
  • FastAPI系列:fastapi定制的数据库操作库sqlmodel
    官网sqlmodel安装#安装sqlmodel会自动安装pydantic和sqlalchemypipinstallsqlmodel使用#步骤1,创建sqlmodel引擎fromsqlmodelimportcreate_engine#driver://用户名:密码@ip/数据库engine=create_engine("mysql+mysqldb://root:123456@localhost/api")#步骤......
  • Taurus.MVC WebMVC 入门开发教程4:数据列表绑定List<Model>
    前言:在本篇Taurus.MVCWebMVC入门开发教程的第四篇文章中,我们将学习如何实现数据列表的绑定,通过使用List<Model>来展示多个数据项。我们将继续使用Taurus.Mvc命名空间,同时探讨如何在视图中绑定并显示一个Model列表。步骤1:创建Model首先,我们需要更新我们的Model类,......