MySQL时间类型与插入更新返回值总结

1.  前言

本文使用的测试数据库版本为“10.0.10-MariaDB-V2.0R131D001-20160907-1111”,InnoDB版本为5.6.15,与MySQL 5.6兼容,可当作MySQL 5.6.15看待。

2.  MySQL时间类型

2.1  DATE、DATETIME与TIMESTAMP类型

参考MySQL文档“11.3.1 The DATE, DATETIME, and TIMESTAMP Types”(https://dev.mysql.com/doc/refman/5.6/en/datetime.html)。

DATE类型只有日期部分,没有时间部分,支持的范围为'1000-01-01'至'9999-12-31'。

DATETIME包含日期部分与时间部分,支持的范围为'1000-01-01 00:00:00'至'9999-12-31 23:59:59'。

TIMESTAMP包含日期部分与时间部分,支持的范围为'1970-01-01 00:00:01' UTC至'2038-01-19 03:14:07' UTC。

DATETIME与TIMESTAMP可以包含微秒精度(6位小数)的小数秒。

包含小数部分在内时,DATETIME与TIMESTAMP的形式为'YYYY-MM-DD HH:MM:SS[.fraction]',DATETIME的范围为'1000-01-01 00:00:00.000000'至'9999-12-31 23:59:59.999999',TIMESTAMP的范围为'1970-01-01 00:00:01.000000'至'2038-01-19 03:14:07.999999'。

2.2  DATETIME与TIMESTAMP的精度

参考MySQL文档“11.1.2 Date and Time Type Overview”(https://dev.mysql.com/doc/refman/5.6/en/date-and-time-type-overview.html)。

MySQL 5.6.4及以上版本中的TIME、DATETIME与TIMESTAMP值支持小数秒,最大支持微秒的精度(6位小数)。

使用“type_name(fsp)”形式定义包含小数秒的时间字段,其中type_name为TIME、DATETIME或TIMESTAMP,fsp为小数秒的精度。

fsp的值必须在0到6之间。当fsp的值为0时,则字段没有小数部分。fsp的默认值为0(这与标准SQL的默认值6不同,为了与之前的MySQL版本兼容)。

DATETIME与TIMESTAMP的默认精度为秒。

MySQL文档“11.3.6 Fractional Seconds in Time Values”(https://dev.mysql.com/doc/refman/5.6/en/date-and-time-type-overview.html)也有关于DATETIME与TIMESTAMP的精度的说明。

2.3  DATETIME与TIMESTAMP的自动初始化及更新

参考MySQL文档“11.3.5 Automatic Initialization and Updating for TIMESTAMP and DATETIME”(https://dev.mysql.com/doc/refman/5.6/en/timestamp-initialization.html)。

2.3.1  MySQL 5.6.5版本及以上

DATETIME与TIMESTAMP字段可以自动初始化并更新为当前时间戳。

对于任意的DATETIME与TIMESTAMP字段,可以将当前时间戳指定为其默认值,或自动更新的值,如下所述:

l  若自动更新字段(DATETIME、TIMESTAMP)在插入时未指定值,则会被设置为当前时间戳;

l  若某行中除自动更新字段外的其他字段的值被修改(与当前值不相同),则自动更新字段会自动更新为当前时间戳。

在列定义中使用DEFAULT CURRENT_TIMESTAMP与ON UPDATE CURRENT_TIMESTAMP子句,可以指定自动更新属性。子句的顺序不影响功能,假如两个子句都在列定义中出现,两者中的任意一个都可以出现在前面。CURRENT_TIMESTAMP的任意同义词具有相同的作用,例如CURRENT_TIMESTAMP()、NOW()、LOCALTIME、LOCALTIME()、LOCALTIMESTAMP与LOCALTIMESTAMP()。

假如DATETIME与TIMESTAMP字段没有明确定义自动更新,则不会具有自动更新属性。但在以下情况中,TIMESTAMP字段默认会具有自动更新属性:

l  explicit_defaults_for_timestamp系统属性未启用;

l  第一个TIMESTAMP字段会同时具有DEFAULT CURRENT_TIMESTAMP与ON UPDATE CURRENT_TIMESTAMP属性,即使没有显示指定任意一个属性。

通过以下策略中的任意一条,可以防止第一个TIMESTAMP字段自动具有DEFAULT CURRENT_TIMESTAMP与ON UPDATE CURRENT_TIMESTAMP属性:

l  启用explicit_defaults_for_timestamp系统变量。在这种情况下,DEFAULT CURRENT_TIMESTAMP与ON UPDATE CURRENT_TIMESTAMP属性的自动初始化与更新仍可用,但TIMESTAMP字段需要明确定义以上属性才会生效。

假如explicit_defaults_for_timestamp系统变量被禁用,则可以通过以下方法中的任意一种解决上述问题:

l  在定义列时,通过DEFAULT子句指定一个固定的默认值(如'2000-01-01 00:00:00');

l  指定NULL属性。同时会使字段允许NULL值,这意味着无法通过设置NULL将字段值指定为当前时间戳。

2.3.2  MySQL 5.6.5版本以下

略。

2.4  日期类型存储占用

参考MySQL文档“11.7 Data Type Storage Requirements”(https://dev.mysql.com/doc/refman/5.6/en/storage-requirements.html)。

对于TIME、DATETIME与TIMESTAMP字段,在MySQL 5.6.4以下版本,与5.6.4及以上版本中,对于存储的占用不相同。这是因为在5.6.4版本中的变化允许以上类型拥有小数部分,可能占用0至3字节。

数据类型

MySQL 5.6.4以下的存储要求

MySQL 5.6.4及以上的存储要求

YEAR

1 字节

1字节

DATE

3字节

3字节

TIME

3字节

3字节+ 小数秒存储占用

DATETIME

8字节

5字节+ 小数秒存储占用

TIMESTAMP

4字节

4字节+ 小数秒存储占用

对于MySQL 5.6.4,YEAR与DATE类型的存储占用没有改变。TIME、DATETIME与TIMESTAMP字段的存储占用则发生了改变。DATETIME被打包得更高效,整数部分需要5个字节而不是8个字节,而且以上三个字段都拥有小数部分,需要0至3个字节,具体占用字节数取决于存储的小数秒值的精度。

小数秒精度

存储要求

0

0 字节

1, 2

1字节

3, 4

2字节

5, 6

3字节

2.5  检查时间毫秒数的函数

某些数据库管理工具在查询DATETIME或TIMESTAMP字段时不会显示小数秒,可通过以下函数查询。

2.5.1  UNIX_TIMESTAMP()

参考MySQL文档“12.7 Date and Time Functions”(https://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_unix-timestamp)。

该函数的调用形式包括UNIX_TIMESTAMP(),UNIX_TIMESTAMP(date)。

当调用无参数形式时,UNIX_TIMESTAMP()返回从'1970-01-01 00:00:00' UTC到当前经过的秒数。

当调用有参数形式时,UNIX_TIMESTAMP(date)返回传入的date参数从'1970-01-01 00:00:00' UTC到当前经过的秒数。

示例如下:

mysql> SELECT UNIX_TIMESTAMP();

        -> 1447431666

mysql> SELECT UNIX_TIMESTAMP('2015-11-13 10:20:19');

        -> 1447431619

mysql> SELECT UNIX_TIMESTAMP('2015-11-13 10:20:19.012');

        -> 1447431619.012

2.5.2  MICROSECOND()

参考MySQL文档“12.7 Date and Time Functions”(https://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_microsecond)。

该函数的调用形式为MICROSECOND(expr)。

当调用该函数时,返回expr参数的微秒数,范围为0至999999。

示例如下:

mysql> SELECT MICROSECOND('12:00:00.123456');

        -> 123456

mysql> SELECT MICROSECOND('2009-12-31 23:59:59.000010');

        -> 10

2.6  获取当前时间的函数

MySQL提供了以下函数用于获取当前时间。

2.6.1  NOW()

参考MySQL文档“12.7 Date and Time Functions”(https://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_now)。

该函数的调用形式为NOW([fsp])。

返回'YYYY-MM-DD HH:MM:SS'或YYYYMMDDHHMMSS格式的当前日期与时间,返回的格式取决于函数使用的上下文是字符串还是数字,返回的时间使用当前时区表示。

从MySQL 5.6.4开始,fsp参数用于指定从0至6的小数秒精度,返回值包含精度与fsp参数相同的小数秒部分。在5.6.4之前的版本中,任何参数都会被忽略。

示例如下:

mysql> SELECT NOW();

        -> '2007-12-15 23:50:26'

mysql> SELECT NOW() + 0;

        -> 20071215235026.000000

2.6.2  与NOW()作用相同的函数

CURRENT_TIMESTAMP()、CURRENT_TIMESTAMP、LOCALTIME()、LOCALTIME、LOCALTIMESTAMP、LOCALTIMESTAMP()。

2.7  与时间类型相关的连接属性

2.7.1  sendFractionalSeconds

参考MySQL文档“5.1 Driver/Datasource Class Names, URL Syntax and Configuration Properties for Connector/J”( https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html)。

指定发送TIMESTAMP的小数秒部分。若该属性为false,则TIMESTAMP的纳秒部分在发送给服务器之前会被截断。该属性适用于预处理语句,可调用的语句,或可更新的结果集。

该属性默认值为true。

从mysql-connector 5.1.37版本开始拥有该属性。

2.7.2  useLegacyDatetimeCode

参考MySQL文档“5.1 Driver/Datasource Class Names, URL Syntax and Configuration Properties for Connector/J”( https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html)。

在结果集与语句中对DATE/TIME/DATETIME/TIMESTAMP进行处理时,使用在客户端与服务器间持续进行时区转换的代码,或者使用已在驱动中对以上数据类型实现向下兼容的传统代码。将此属性设置为false会导致"useTimezone"、"useJDBCCompliantTimezoneShift"、"useGmtMillisForDatetimes"及"useFastDateParsing"属性失效。

该属性默认值为true。

从mysql-connector 5.1.6版本开始拥有该属性。

2.7.3  useSSPSCompatibleTimezoneShift

参考MySQL文档“5.1 Driver/Datasource Class Names, URL Syntax and Configuration Properties for Connector/J”( https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html)。

如果从使用服务器端预处理语句的环境迁移而来,且将配置属性useJDBCCompliantTimeZoneShift设置为true,则在将TIMESTAMP值发送至MySQL服务器时,如果不使用服务器端预处理语句,则使用兼容行为。

该属性默认值为false。

从mysql-connector 5.0.5版本开始拥有该属性。

3.  MySQL插入语句返回值

3.1  insert

略。

3.2  insert ignore

插入时若出现错误会被忽略,例如主键或唯一索引出现重复数据。

返回行数为实际执行了插入的数据行数。

3.3  insert on duplicate key update

当不存在主键或唯一索引相同的数据时,执行插入操作。

当存在主键或唯一索引相同的数据时,执行更新操作。

返回行数如下(MySQL的jdbc连接参数使用useAffectedRows=true时的结果,若为false时返回的行数会不同):

l  当执行插入时,返回插入的行数;

l  当执行更新时,返回更新的行数*2;

l  当未执行插入或更新时,返回0;

l  批量处理时,返回以上记录总和。

3.4  replace into

当不存在主键或唯一索引相同的数据时,执行插入操作。

当存在主键或唯一索引相同的数据时,先删除旧数据再插入。

根据MySQL文档说明,返回受影响行数,即删除与插入的数据总和。

根据MySQL实际测试结果,返回行数情况如下:

l  插入不存在主键或唯一索引相同的数据,返回对应记录行数;

l  插入存在主键或唯一索引、其他字段均相同的数据,返回对应记录行数;

l  插入存在主键或唯一索引相同,但其他字段不同的数据,返回对应记录行数*2;

l  批量处理时,返回以上记录总和。

4.  MySQL更新语句返回值

4.1  UPDATE返回值含义

参考MySQL文档“13.2.11 UPDATE Syntax”(https://dev.mysql.com/doc/refman/5.6/en/update.html)。

UPDATE返回实际变化的行数。

4.2  UPDATE后数据未修改的情况

参考MySQL文档“5.7.1.12 Statement Probes”(https://dev.mysql.com/doc/refman/5.6/en/dba-dtrace-ref-statement.html)。

对于UPDATE与UPDATE t1,t2 ...语句,提供了匹配的行数及实际变化的行数。匹配的行数是指对应的WHERE子句匹配的行数,与实际变化的行数可能会不同。

当某一行在update前后的值相同时,MySQL不会对该行的值进行更新。

4.3  useAffectedRows连接属性

useAffectedRows是MySQL的连接属性。

参考MySQL文档“Chapter 6 Connector/Net Connection-String Options Reference”(https://dev.mysql.com/doc/connector-net/en/connector-net-connection-options.html)。

当useAffectedRows属性为true时,连接返回改变的行数,而不是找到的行数。

查看mysql-connector-java-xxx.jar,在com.mysql.jdbc.ConnectionPropertiesImpl类中有如下代码:

private BooleanConnectionProperty useAffectedRows = new BooleanConnectionProperty("useAffectedRows", false,

            Messages.getString("ConnectionProperties.useAffectedRows"), "5.1.7", MISC_CATEGORY, Integer.MIN_VALUE);

在com.mysql.jdbc.MysqlIO类的doHandshake方法中,包含如下代码:

        if (!this.connection.getUseAffectedRows()) {

            this.clientParam |= CLIENT_FOUND_ROWS;

        }

当useAffectedRows参数为假时,使用CLIENT_FOUND_ROWS属性。

4.4  CLIENT_FOUND_ROWS属性

参考MySQL文档,对于UPDATE语句,当连接至MySQL服务器调用mysql_real_connect()函数时,若指定了CLIENT_FOUND_ROWS标志,则会返回WHERE子句匹配的行数;若未指定该标志,则返回实际改变的行数。

5.  问题记录

以下为开发过程中遇到的相关问题记录。

5.1  相关版本

组件

版本号

MariaDB

10.0.10-MariaDB-V2.0R131D001-20160907-1111

InnoDB

5.6.15

mysql:mysql-connector-java

5.1.44

org.mybatis:mybatis

3.4.5

org.mybatis:mybatis-spring

1.3.1

com.alibaba:druid

1.1.5

5.2  TIMESTAMP自动更新

5.2.1  问题描述

在建表时使用了以下语句(省略不相关字段):

CREATE TABLE test11 (

    create_time TIMESTAMP NOT NULL COMMENT '创建时间',

    update_time TIMESTAMP NOT NULL COMMENT '更新时间'

) ENGINE=InnoDB;

在开发时发现对数据库表进行其他字段的更新时,create_time字段值也会自动更新。

5.2.2  问题分析

查看对应数据库表的建表语句如下:

CREATE TABLE `test11` (

  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',

  `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间'

) ENGINE=InnoDB DEFAULT CHARSET=utf8

可以发现create_time字段包含DEFAULT CURRENT_TIMESTAMP与ON UPDATE CURRENT_TIMESTAMP属性,导致了create_time字段值会自动更新。

根据“2.3 DATETIME与TIMESTAMP的自动初始化及更新”对应的MySQL文档中的说明,查询数据库的explicit_defaults_for_timestamp属性,发现该属性不存在,即未启用,会导致数据库表的第一个TIMESTAMP字段会同时具有DEFAULT CURRENT_TIMESTAMP与ON UPDATE CURRENT_TIMESTAMP属性。

5.2.3  问题解决

由于数据库的explicit_defaults_for_timestamp属性不能修改,因此可通过以下方式解决以上问题:

l  将日期字段设置为允许为NULL

将建表语句修改为如下:

CREATE TABLE test12 (

    create_time TIMESTAMP NULL COMMENT '创建时间',

    update_time TIMESTAMP NULL COMMENT '更新时间'

) ENGINE=InnoDB;

创建数据库表后对应数据库表的建表语句如下:

CREATE TABLE `test12` (

  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',

  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间'

) ENGINE=InnoDB DEFAULT CHARSET=utf8

解决了TIMESTAMP字段自动更新的问题,但允许日期字段为空,因此不使用该方法。

l  使用DATETIME类型的日期字段

将建表语句修改为如下:

CREATE TABLE test13 (

    create_time DATETIME NOT NULL COMMENT '创建时间',

    update_time DATETIME NOT NULL COMMENT '更新时间'

) ENGINE=InnoDB;

创建数据库表后对应数据库表的建表语句如下:

CREATE TABLE `test13` (

  `create_time` datetime NOT NULL COMMENT '创建时间',

  `update_time` datetime NOT NULL COMMENT '更新时间'

) ENGINE=InnoDB DEFAULT CHARSET=utf8

DATETIME类型不存在自动更新的特性,通过该方法解决了上述问题。

5.3  时间精度为秒导致UPDATE返回行数为0

5.3.1  问题描述

数据库表中包含批次号与更新时间等字段,建表语句示例如下(省略不相关字段):

CREATE TABLE test21 (

    id VARCHAR(20) NOT NULL COMMENT 'ID',

    batch_no VARCHAR(20) NOT NULL COMMENT '批次号',

    update_time DATETIME NOT NULL COMMENT '更新时间'

) ENGINE=InnoDB;

在Java程序的jdbc.url参数中,指定了useAffectedRows=true,返回发生改变的行数。

存在一个Java方法,调用SQL语句更新上述表的批次号与更新时间字段,更新时通过主键进行限制范围,正常情况下每次都应返回1。调用的SQL语句如下所示:

UPDATE test21 set batch_no = xxx ,update_time = yyy WHERE id = nnn;

在开发时发现,上述Java方法在执行时,有时返回1,有时返回0。

当出现上述Java方法返回0的情况时,UPDATE前后,满足条件的行的batch_no字段未改变。

5.3.2  问题分析

感谢DB-Helper(DB-Helper)帮忙定位该问题。

出现以上问题的原因,是因为前后两次更新的时间过于接近,在同一秒内,由于update_time在定义时未指定精度,因此该字段为秒级,当在某一秒内进行多次更新时,实际上UPDATE前后update_time字段值并未改变;且batch_no字段在UPDATE后未改变,因此MySQL不会对对应的行进行更新操作,返回改变的行数为0。

通过以下示例重现该问题:

向上述表中插入测试数据:

insert into test21 values('id','test','2018-03-26 21:12:34.123');

更新上述测试数据:

UPDATE test21 set batch_no = 'test' ,update_time = '2018-03-26 21:12:34.789' WHERE id = 'id';

mysql命令输出如下,显示匹配行数为1,但改变行数为0,因此在对应的Java方法中,返回值为0。

Query OK, 0 rows affected (0.00 sec)

Rows matched: 1  Changed: 0  Warnings: 0

查询上述数据库表的update_time字段,发现秒没有小数部分。

mysql> select UNIX_TIMESTAMP(update_time) from test21 where id = 'id';

+-----------------------------+

| UNIX_TIMESTAMP(update_time) |

+-----------------------------+

|                  1522069954 |

+-----------------------------+

1 row in set (0.00 sec)

mysql> select MICROSECOND(update_time) from test21 where id = 'id';

+--------------------------+

| MICROSECOND(update_time) |

+--------------------------+

|                        0 |

+--------------------------+

1 row in set (0.00 sec)

5.3.3  问题解决

在定义数据库表时,为DATETIME字段指定支持小数秒,所下所示:

CREATE TABLE test22 (

    id VARCHAR(20) NOT NULL COMMENT 'ID',

    batch_no VARCHAR(20) NOT NULL COMMENT '批次号',

    update_time DATETIME(3) NOT NULL COMMENT '更新时间'

) ENGINE=InnoDB;

向上述表中插入测试数据:

insert into test22 values('id','test','2018-03-26 21:12:34.123');

更新上述测试数据:

UPDATE test22 set batch_no = 'test' ,update_time = '2018-03-26 21:12:34.789' WHERE id = 'id';

mysql命令输出如下,显示匹配行数为1,改变行数也是1。

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

查询上述数据库表的update_time字段,发现已包含小数秒部分,解决该问题。

mysql> select UNIX_TIMESTAMP(update_time) from test22 where id = 'id';

+-----------------------------+

| UNIX_TIMESTAMP(update_time) |

+-----------------------------+

|              1522069954.789 |

+-----------------------------+

1 row in set (0.01 sec)

mysql> select MICROSECOND(update_time) from test22 where id = 'id';

+--------------------------+

| MICROSECOND(update_time) |

+--------------------------+

|                   789000 |

+--------------------------+

1 row in set (0.00 sec)

5.4  new Date()无法写入小数秒时间

5.4.1  问题描述

创建数据库表test31,update_time字段类型为DATETIME(3),保存3位小数秒,如下所示:

CREATE TABLE test31 (

    id varchar(32) NOT NULL COMMENT 'ID',

    update_time DATETIME(3) NOT NULL COMMENT '更新时间',

    PRIMARY KEY (id)

) ENGINE=InnoDB;

Java代码的entity类(省略get/set方法):

public class Test31 {

    private String id;

    private Date update_time;

}

Java代码的DAO的方法(省略不相关方法):

public interface Test31Mapper {

    int updateByPrimaryKey(Test31 record);

}

Mapper XML内容(省略不相关内容):

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="test.sql.dao.Test31Mapper">

  <update id="updateByPrimaryKey" parameterType="test.sql.entity.Test31">

    update test31

    set update_time = #{update_time,jdbcType=TIMESTAMP}

    where id = #{id,jdbcType=VARCHAR}

  </update>

</mapper>

在测试代码中,对test31表的update_time字段进行了更新,如下所示:

    public void test() {

        Test31 test31 = new Test31();

        test31.setId("id");

        test31.setUpdate_time(new Date());

 

        int row = test31Mapper.updateByPrimaryKey(test31);

 

        logger.info("row: {}", row);

    }

查询test31表内容,update_time字段不包含小数秒:

mysql> select id,unix_timestamp(update_time) from test31;

+----+-----------------------------+

| id | unix_timestamp(update_time) |

+----+-----------------------------+

| id |              1522142289.000 |

+----+-----------------------------+

1 row in set (0.00 sec)

向test31表insert数据时,update_time字段也不包含小数秒,问题原因相同,不再重复。

5.4.2  问题分析

5.4.2.1     日志分析

查看应用日志,可以看到应用传递的update_time参数为“2018-03-27 17:18:09.456”,包含小数秒:

2018-03-27 17:18:09.474 [main] DEBUG BaseJdbcLogger.debug(159) - ==>  Preparing: update test31 set update_time = ? where id = ?

2018-03-27 17:18:09.575 [main] DEBUG BaseJdbcLogger.debug(159) - ==> Parameters: 2018-03-27 17:18:09.456(Timestamp), id(String)

2018-03-27 17:18:09.583 [main] DEBUG BaseJdbcLogger.debug(159) - <==    Updates: 1

5.4.2.2     抓包分析

使用Wireshark对本地Java程序与MySQL服务器的通信内容抓包分析,可以看到本地向MySQL服务器发送的SQL语句中,设置update_time为“2018-03-27 17:18:09”,没有包含小数秒。说明通过上述代码更新test31表的update_time字段时,实际上未向MySQL服务器发送小数秒部分。

通信内容截图如下所示:

5.4.2.3     mysql-connector源码分析

在mysql-connector-java-5.1.44.jar的com.mysql.jdbc.PreparedStatement类的“public void setTimestamp(int parameterIndex, Timestamp x)”方法中,调用了setTimestampInternal方法。

setTimestampInternal方法代码如下:

private void setTimestampInternal(int parameterIndex, Timestamp x, Calendar targetCalendar, TimeZone tz, boolean rollForward) throws SQLException {
    
if (x == null) {
        setNull(parameterIndex, java.sql.Types.
TIMESTAMP);
    } 
else {
        checkClosed();

        
if (!this.sendFractionalSeconds) {
            x = TimeUtil.truncateFractionalSeconds(x);
        }

        
if (!this.useLegacyDatetimeCode) {
            newSetTimestampInternal(parameterIndex, x, targetCalendar);
        } 
else {
            Calendar sessionCalendar = 
this.connection.getUseJDBCCompliantTimezoneShift() ? this.connection.getUtcCalendar()
                    : getCalendarInstanceForSessionOrNew();

            x = TimeUtil.changeTimezone(
this.connection, sessionCalendar, targetCalendar, x, tz, this.connection.getServerTimezoneTZ(), rollForward);

            
if (this.connection.getUseSSPSCompatibleTimezoneShift()) {
                doSSPSCompatibleTimezoneShift(parameterIndex, x);
            } 
else {
                
synchronized (this) {
                    
if (this.tsdf == null) {
                        
this.tsdf new SimpleDateFormat("''yyyy-MM-dd HH:mm:ss", Locale.US);
                    }

                    StringBuffer buf = 
new StringBuffer();
                    buf.append(
this.tsdf.format(x));

                    
if (this.serverSupportsFracSecs) {
                        
int nanos = x.getNanos();

                        
if (nanos != 0) {
                            buf.append(
'.');
                            buf.append(TimeUtil.formatNanos(nanos, 
this.serverSupportsFracSecstrue));
                        }
                    }

                    buf.append(
'\'');

                    setInternal(parameterIndex, buf.toString()); 
// SimpleDateFormat is not
                                                                // thread-safe
                
}
            }
        }

        
this.parameterTypes[parameterIndex - + getParameterIndexOffset()] = Types.TIMESTAMP;
    }
}

sendFractionalSeconds变量对应com.mysql.jdbc.ConnectionPropertiesImpl类的sendFractionalSeconds变量,对应MySQL连接属性sendFractionalSeconds(见前文),ConnectionPropertiesImpl类变量定义如下所示:

    private BooleanConnectionProperty sendFractionalSeconds = new BooleanConnectionProperty("sendFractionalSeconds", true,

            Messages.getString("ConnectionProperties.sendFractionalSeconds"), "5.1.37", MISC_CATEGORY, Integer.MIN_VALUE);

sendFractionalSeconds属性默认为true,因此不会执行TimeUtil.truncateFractionalSeconds方法。该方法会将TIMESTAMP的纳秒部分截断,因此默认情况不会截断。

useLegacyDatetimeCode变量对应ConnectionPropertiesImpl类的sendFractionalSeconds变量,对应MySQL连接属性useLegacyDatetimeCode(见前文),ConnectionPropertiesImpl类变量定义如下所示:

    private BooleanConnectionProperty useLegacyDatetimeCode = new BooleanConnectionProperty("useLegacyDatetimeCode", true,

            Messages.getString("ConnectionProperties.useLegacyDatetimeCode"), "5.1.6", MISC_CATEGORY, Integer.MIN_VALUE);

useLegacyDatetimeCode属性默认为ture,因此不会执行newSetTimestampInternal方法,会进入else分支中。

在else分支中,以下代码不会对传入的“Timestamp x”变量的小数秒部分产生影响,因此忽略:

                Calendar sessionCalendar = this.connection.getUseJDBCCompliantTimezoneShift() ? this.connection.getUtcCalendar()

                        : getCalendarInstanceForSessionOrNew();

 

                x = TimeUtil.changeTimezone(this.connection, sessionCalendar, targetCalendar, x, tz, this.connection.getServerTimezoneTZ(), rollForward);

connection.getUseSSPSCompatibleTimezoneShift()方法对应ConnectionPropertiesImpl类的useSSPSCompatibleTimezoneShift变量,对应MySQL连接属性useSSPSCompatibleTimezoneShift(见前文),ConnectionPropertiesImpl类变量定义如下所示:

    private BooleanConnectionProperty useSSPSCompatibleTimezoneShift = new BooleanConnectionProperty("useSSPSCompatibleTimezoneShift", false,

            Messages.getString("ConnectionProperties.useSSPSCompatibleTimezoneShift"), "5.0.5", MISC_CATEGORY, Integer.MIN_VALUE);

useSSPSCompatibleTimezoneShift属性默认为false,因此不会执行doSSPSCompatibleTimezoneShift方法,会进入下一个else分支中。

在下一个else分支中,对传入的“Timestamp x”变量使用“yyyy-MM-dd HH:mm:ss”进行了格式化,并拼接至“StringBuffer buf”变量中。

若serverSupportsFracSecs变量为true,则将传入的“Timestamp x”变量的纳秒部分拼接至“StringBuffer buf”变量中。

最后调用setInternal方法处理buf变量。

由此可见,mysql-connector中的客户端代码在更新日期类型时是否会将小数秒部分发送至MySQL服务器,取决于PreparedStatement类的serverSupportsFracSecs变量。当该变量为true时则会发送,否则不发送。

在PreparedStatement类的构造方法中,均会调用detectFractionalSecondsSupport方法。

PreparedStatement类的detectFractionalSecondsSupport方法代码如下:

this.serverSupportsFracSecs = this.connection != null && this.connection.versionMeetsMinimum(5, 6, 4);

当与MySQL服务器连接成功,且MySQL服务器版本大于等于5.6.4时,serverSupportsFracSecs变量为true;即MySQL服务器版本大于等于5.6.4时,mysql-connector中的客户端代码在更新日期类型时会将小数秒部分发送至MySQL服务器,否则不会发送。

this.connection.versionMeetsMinimum()方法对应com.mysql.jdbc.ConnectionImpl类的versionMeetsMinimum方法。

ConnectionImpl类的versionMeetsMinimum方法调用了com.mysql.jdbc.MysqlIO类的versionMeetsMinimum方法。

在MysqlIO类的versionMeetsMinimum方法中,分别调用getServerMajorVersion、getServerMinorVersion、getServerSubMinorVersion方法获取MySQL的主版本号、次版本号、子版本号。以上方法分别对应MysqlIO类的serverMajorVersion、serverMinorVersion、serverSubMinorVersion变量。

在MysqlIO类的doHandshake方法中,将MySQL服务器版本号保存在serverVersion变量中,之后再处理serverMajorVersion、serverMinorVersion、serverSubMinorVersion变量。

5.4.2.4     mysql-connector调试分析

根据上文分析,在Java程序连接MySQL服务器时,调试MysqlIO类的doHandshake方法。

如下图所示,MysqlIO类的serverVersion变量为“5.5.5-10.0.10-proxy”。

继续执行后,serverMajorVersion、serverMinorVersion、serverSubMinorVersion变量均为5,如下图所示。

如上所述,在PreparedStatement方法中,this.connection.versionMeetsMinimum(5, 6, 4)结果为false,因此不会将时间类型的小数部分发送至MySQL服务器。

在连接MySQL时,需要先连接DB Proxy,由DB Proxy实际连接MySQL服务器。

“5.5.5-10.0.10-proxy”为DB Proxy的版本号。

telnet DB Proxy的服务端口时,可在返回内容中看到以上版本号,如下图所示:

5.4.3  问题解决

由于无法修改DB Proxy的版本,因此需要通过修改代码的方式解决以上问题。

在Java代码的DAO增加方法:

public interface Test31Mapper {

    int updateByPrimaryKey2(Test31 record);

}

在Mapper XML中增加以下内容:

  <update id="updateByPrimaryKey2" parameterType="test.sql.entity.Test31">

    update test31

    set update_time = now(3)

    where id = #{id,jdbcType=VARCHAR}

  </update>

在测试代码中,调用新增方法对test31表进行更新,如下所示:

    public void test2() {

        Test31 test31 = new Test31();

        test31.setId("id");

 

        int row = test31Mapper.updateByPrimaryKey2(test31);

 

        logger.info("row: {}", row);

    }

查询test31表内容,update_time字段已包含小数秒,解决该问题。

mysql> select id,unix_timestamp(update_time) from test31;

+----+-----------------------------+

| id | unix_timestamp(update_time) |

+----+-----------------------------+

| id |              1522156789.667 |

+----+-----------------------------+

1 row in set (0.01 sec)

使用Wireshark对本地Java程序与MySQL服务器的通信内容抓包分析,可以看到本地向服务器发送的SQL语句中,设置update_time为“now(3)”。

通信内容截图如下所示:

因此,在Mapper XML中,在更新或插入日期字段时,指定为now(x),可向数据库写入小数秒。

发布了37 篇原创文章 · 获赞 0 · 访问量 2322

猜你喜欢

转载自blog.csdn.net/a82514921/article/details/104609845
今日推荐