對(duì)于數(shù)據(jù)庫(kù)的日期字段,一般會(huì)使用Date類(lèi)型,比如下面的這個(gè)建表語(yǔ)句:
CREATE TABLE `record` (
`id` BIGINT UNSIGNED NOT NULL PRIMARY KEY PRIMARY KEY AUTO_INCREMENT
COMMENT '自增id',
`operator_id` BIGINT UNSIGNED DEFAULT 0
COMMENT '操作者id',
`operator_name` VARCHAR(128) NOT NULL
COMMENT '操作者名字',
`date` DATE NOT NULL
COMMENT '日期', #2017-03-23 方便按天查詢(xún)
)
ENGINE = InnoDB;
在編寫(xiě)和這個(gè)date對(duì)應(yīng)的model的時(shí)候,一般會(huì)使用sql.Date。比如下面這樣:
@Data
public class Record {
private long id;
private long operatorId;
private String operatorName;
java.sql.Date date; //使用sql.Date 類(lèi)型
}
這樣,在數(shù)據(jù)的寫(xiě)入和查詢(xún)時(shí),都不會(huì)有任何問(wèn)題。但是最近,我們把sql.Date切換成了util.Date,如下面這樣:
@Data
public class Record {
private long id;
private long operatorId;
private String operatorName;
java.util.Date date; //使用util.Date 類(lèi)型代替sql.Date
}
切換成util.Date后,在插入數(shù)據(jù)和查詢(xún)時(shí)也沒(méi)有問(wèn)題,但是在比較date字段的時(shí)候,發(fā)現(xiàn)了一個(gè)詭異的問(wèn)題。如下面代碼:
Date today = new Date();
Record record = dao.getByDate(...);
if (record.getDate().equals(today)) { //false
...
}
為了定位這個(gè)問(wèn)題,我們首先懷疑是RowMapper映射的時(shí)候綁定出了問(wèn)題,將sql.Date的值綁給了util.Date,引發(fā)不相等。
我們先來(lái)看sql.Date的源碼??此蛈til.Date有什么區(qū)別,如下:
public class Date extends java.util.Date {
....
}
sql.Date繼承自u(píng)til.Date,而且沒(méi)有override equal的實(shí)現(xiàn),我們可以看一下util.Date的equals方法的實(shí)現(xiàn):
public boolean equals(Object obj) {
return obj instanceof Date && getTime() == ((Date) obj).getTime();
}
可以看到,如果是sql.Date和util.Date做equal比較,那么應(yīng)該是相等的。所以問(wèn)題應(yīng)該不是sql.Date和util.Date類(lèi)型不同造成的。
我們繼續(xù)懷疑RowMapper綁定的時(shí)候做了特殊處理,那么繼續(xù)看RowMapper的實(shí)現(xiàn),我們?cè)贘dbcUtils里發(fā)現(xiàn)下面的代碼( org.springframework.jdbc.support.JdbcUtils):
else if (java.sql.Date.class == requiredType) {
return rs.getDate(index);
}
else if (java.sql.Time.class == requiredType) {
return rs.getTime(index);
}
else if (java.sql.Timestamp.class == requiredType || java.util.Date.class == requiredType) {
return rs.getTimestamp(index);
}
可以看到,在把sql.Date綁定到util.Date時(shí),觸發(fā)了這段邏輯:
else if (java.sql.Timestamp.class == requiredType || java.util.Date.class == requiredType) {
return rs.getTimestamp(index);
}
換句話(huà)說(shuō),我們model里的util.Date被綁定上了一個(gè)sql.Timestamp類(lèi)型,我們來(lái)看sql.Timestamp的實(shí)現(xiàn):
public class Timestamp extends java.util.Date {
...
public boolean equals(java.lang.Object ts) {
if (ts instanceof Timestamp) {
return this.equals((Timestamp)ts);
} else {
return false;
}
}
}
這里可以看出,如果是timestamp類(lèi)型和util.Date類(lèi)型做比較,則一定會(huì)返回false。
至此,問(wèn)題定位。
對(duì)于這個(gè)問(wèn)題的反思,我覺(jué)得主要有幾點(diǎn):
1 java的date類(lèi)型的整體設(shè)計(jì),確實(shí)非常不好,里面的各種邏輯非?;靵y,比如Date可以保存時(shí)分秒,還有時(shí)區(qū)引起的其他坑等等,因此應(yīng)該盡量的避免使用util.Date類(lèi)型
2 對(duì)于model層使用時(shí)間,如果確實(shí)有Date類(lèi)型的需求,請(qǐng)使用sql.Date,不要使用util.Date。sql.Date在使用過(guò)程中的表現(xiàn)基本是一致的,而且屏蔽了時(shí)分秒的問(wèn)題
3 如果數(shù)據(jù)庫(kù)在設(shè)計(jì)中有夸時(shí)區(qū)的可能,應(yīng)盡量避免使用Date類(lèi)型,而是保持一個(gè)long的時(shí)間戳,在業(yè)務(wù)層屏蔽混亂的Date類(lèi)型