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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章