sql.Date和util.Date混用引發(fā)的bug

對(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)型

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容