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 问题分析
查看应用日志,可以看到应用传递的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 |
使用Wireshark对本地Java程序与MySQL服务器的通信内容抓包分析,可以看到本地向MySQL服务器发送的SQL语句中,设置update_time为“2018-03-27 17:18:09”,没有包含小数秒。说明通过上述代码更新test31表的update_time字段时,实际上未向MySQL服务器发送小数秒部分。
通信内容截图如下所示:
在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 { |
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变量。
根据上文分析,在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),可向数据库写入小数秒。