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),可向數據庫寫入小數秒。