JDBC和Ibatis中的Date,Time,Timestamp處理


在此前,遇到過使用Ibatis操作Oracle時時間精度丟失的問題,昨天又遇到JDBC操作MySQL時間字段的問題,從網上看到各種式樣的解釋這些問題的博文/帖子,但多是霧里看花,不得要領。

  1. 理解JDBC中的時間類型
  2. MySQL與JDBC之間的類型映射
  3. Oracle與JDBC之間的類型映射
  4. Ibatis是怎么處理日期時間類型的
  5. 注釋
  6. 參考資料

理解JDBC中的時間類型

java.sql包中包括三個類,DateTime, 和 Timestamp,分別用來表示日期(無時間信息,eg: YYYY-MM-DD),時間(只處理時間,無日期部分, eg: HH:MM:SS)和時間戳(精確到納秒級別)。在 它們都繼承自java.util.Date。java.sql.Date和java.sql.Time都存有一個時間域,該時間域是精確到秒級別的,但是,他們只處理日期或者時間部分。在MySQL Connector/J(v5.1.13)的實現中可以看到,PrepareStatement#setDate 時會將時間重新format成”yyyy-MM-dd”格式。

因為歷史遺留以及各種數據庫本身的不同,各種JDBC實現中留下了格式各樣的花招, 某些特殊場景下不遵照此情況可能也能工作,但不推薦這樣做。

你必須根據你的實際需要來選擇不同的類型,通常應該選用精確度與相應的數據庫字段類型相比相同或者更高的JDBC類型

除此以外,還可以使用java.uitl.Date類型來處理時間。java.util.Date類型是上面各個類型的父類型,JDBC的API大都可以使用。

除此以外,在JDK1.6之前版本的構造java.sql.{Date|Time|TimeStamp}對象時存在性能問題,尤其是在多線程環境下更嚴重。這個Bug在這里。其原因是java.util.Date的相關方法調用了TimeZone.getDefaultRef(),而此方法是同步方法注1

MySQL的JDBC類型映射

DATE java.sql.Date
DATETIME java.sql.Timestamp
TIMESTAMP[(M)] java.sql.Timestamp
TIME java.sql.Time
YEAR[(2|4)] If yearIsDateType configuration property is set to false, then
the returned object type is java.sql.Short. If set to true (the
default) then an object of type java.sql.Date (with the date
set to January 1st, at midnight).

MySQL的DATETIME、TIMESTAMP兩種字段類型的顯著區別在於TIMESTAMP的取值在’1970-01-01 00:00:01′ UTC 和 ‘2038-01-19 03:14:07′ UTC之間。

MySQL在時間處理方面也有一個問題,當Datetimes類型的字段的值為0000-00-00時取值方法會得到下面的異常:

Cannot convert value '0000-00-00 00:00:00' from column xx to TIMESTAMP

這個問題的原因在於,MySQL中默認使用0000-00-00等來表示時間的特殊值(參見文檔)。而在Java中,並沒有一個合適的方式來表示這個時間(因為Java中時間軸上0是1970-01-01 00:00:00),早於這個時間的用負數表示,這個最小的負數在時間軸上是表示不出來的。Connector/J提供了一個屬性zeroDateTimeBehavior來解決此問題。

  • exception (the default), which throws an SQLException with an SQLState of S1009.(默認行為)
  • convertToNull, which returns NULL instead of the date.
  • round, which rounds the date to the nearest closest value which is 0001-01-01.

如下所示的jdbc連接將指定該行為轉化為null值。

jdbc:mysql://localhost/myDatabase?zeroDateTimeBehavior=convertToNull

Oracle與JDBC之間的類型映射

DATE java.sql.Date
DATE java.sql.Time
TIMESTAMP java.sql.Timestamp

Oracle數據庫字段類型主要有DATE、TIMESTAMP。

在9i以后、11g以前的Oracle JDBC驅動中存在一個會丟失DATE類型字段的時間信息的bug,原因是其JDBC驅動將Oracle的Date類型處理為java.sql.Date類型,這就丟失了時間部分(看來對java.sql包下三種時間類型認識不足的問題還很普遍的)。關於這個問題,這篇帖子給出了不錯的解釋(牆外),此文中的方法適用於11g JDBC以前的各版本驅動。在11g JDBC驅動中,此問題已經解決了,Oracle 11g JDBC驅動的手冊中也給出了解釋注2

事實上,如果是使用Ibatis,pojo屬性的類型設置為java.util.Date,確保 jdbcType不為 DATE或者TIME,則避免了這個bug。因為此時Ibatis會以java.sql.Timestamp來處理該字段。我專門對此做了驗證,點此看測試項目源碼

Ibatis是怎么處理日期類型的

注意:本文皆基於ibatis 2.3.4.726源碼分析。不過根據我初略觀察,Ibatis3也適用,但請遇到問題時有所留意。

在此前工作中碰到Oracle中的Date字段會只剩下日期部分的數據,丟失了,Google發現一些人的解決方法是將JDBCType指定為datetime。有人甚至自己編寫一個自定義的TypeHandler來解決這個問題。其實這完全是瞎貓撞上死耗子,那個datetime根本沒意義,卻歪打正着。一般的錯誤都是如下的配置(或者是pojo的屬性為java.sql.Date類型):

<sqlMap namespace="Info" >
  <resultMap id="Info" class="pojo.Info" >
    <result column="INFO_BEGINTIME" property="begin" jdbcType="DATE" />
    <result column="INFO_ENDTIME" property="end" jdbcType="DATE" />
  </resultMap>

此時不論你pojo.Info中的字段類型(或者的javaType屬性)是java.util.Date還是java.sql.Date,最終都會丟失數據。實際上,在pojo.Info中的字段類型(或者javaType屬性)為java.util.Date時,如果指定jdbcType為DATE或TIME(區分大小寫),則將分別得到DateOnlyTypeHandler或TimeOnlyTypeHandler。如果你不指定jdbcType,或者指定一個錯誤的值,例如datetime,或者xxxx等,都會得到DateTypeHandler(日期時間都會處理)。

如果pojo.Info中的屬性類型(或者配置中的javaType屬性)是java.sql.Date,則選擇處理handler類時不會使用jdbcType,而是根據屬性類型得到SqlDateTypeHandler(只包含日期)。

在Ibatis的手冊中,指出來jdbcType一般情況下是不用於判斷處理方式的注3。大部分情況下是依據javaType或者pojo屬性類型來判斷處理方式,在少部分無法根據pojo屬性類型判斷的情況下才使用。如果你用java.util.Date對應到MySQL,則就是這種少數情況之一。因為MySQL可以將多個數據庫字段類型映射到java.util.Date,所以需要指定是DATE還是TIME(必須是大寫),如果不指定則默認是完整的日期時間(此時按JDBC的timestamp操作)。

針對ibatis對時間的處理,我寫了個測試,點此看測試代碼。

對於Ibatis操作Date/Time/DateTime,總結如下:

  • 將pojo的屬性類型設置為java.sql.Date(或java.sql.Time, java.sql.Timestamp),此時會嚴格遵循這三種類型的語義。但此方法因存在前文中提到的性能問題,在JDK1.6以前的JDK版本中能少使用就少使用。
  • 如果你想在pojo中使用java.util.Date, 則要注意:
    • 完整的日期時間,要確保jdbcType為空,或為DATE,TIME以外的值
    • 只需要時間,要指定jdbcType=”TIME”
    • 只需要日期,要指定jdbcType=”DATE”

注釋

注釋1:Use oracle.sql.DATE or oracle.sql.TIMESTAMP rather than
java.sql.Date or java.sql.Timestamp if you are using JDK 1.5 or earlier versions or require maximum performance. You may also use the oracle.sql data type if you want to read many date values or compute or display only a small percentage. Due to a bug in all versions of Java prior to JDK 1.6, construction of java.lang.Date and java.lang.Timestamp objects is slow, especially in multithreaded environments. This bug is fixed in JDK 1.6.

注釋2:Mapping SQL DATE Data type to Java Oracle Database 8i and earlier versions did not support TIMESTAMP data, but Oracle DATE data used to have a time component as an extension to the SQL standard. So, Oracle Database 8i and earlier versions of JDBC drivers mapped oracle.sql.DATE to java.sql.Timestamp to preserve the time component. Starting with Oracle Database 9.0.1, TIMESTAMP support was included and 9i JDBC drivers started mapping oracle.sql.DATE to java.sql.Date. This mapping was incorrect as it truncated the time component of Oracle DATE data. To overcome this problem, Oracle Database 11.1 introduces a new flag mapDateToTimestamp. The default value of this flag is true, which means that by default the drivers will correctly map oracle.sql.DATE to java.sql.Timestamp, retaining the time information. If you still want the incorrect but 10g compatible oracle.sql.DATE to java.sql.Date mapping, then you can get it by setting the value of mapDateToTimestamp flag to false.

注釋3:Ibatis 2的手冊中給出的jdbcType屬性的解釋:屬性jdbcType用於顯式地指定給本屬性(property)賦值的數據庫字段的數據類型。對於某些特定的操作,如果不指定字段的數據類型,某些JDBC Driver無法識別字段的數據類型。一個很好的例子是PreparedStatement.setNull(int parameterIndex, int sqlType)方法,要求指定數據類型。如果不指定數據類型,某些Driver可能指定為Types.Other或Types.Null。但是,不能保證所有的Driver都表現一致。對於這種情況,SQL Map API允許使用parameterMap子元素parameter的jdbcType屬性指定數據類型。
正常情況下,只有當字段可以為NULL時才需要jdbcType屬性。另一需要指定jdbcType屬性的情況是字段類型為日期時間類型的情況。因為Java只有一個Date類型(java.util.Date),而大多數SQL數據庫有多個-通常至少有3種。因此,需要指定字段類型是DATE還是DATETIME。
屬性jdbcType可以是JDBC Types類中定義的任意參數的字符串值。雖然如此,還是有某些類型不支持(即BLOB)。本節的稍后部分會說明架構支持的數據類型。
注意!大多數JDBC Driver只有在字段可以為NULL時需要指定jdbcType屬性。因此,對於這些Driver,只是在字段可以為NULL時才需要指定type屬性。
注意!當使用Oracle Driver時,如果沒有給可以為NULL的字段指定jdbcType屬性,當試圖給這些字段賦值NULL時,會出現“Invalid column type”錯誤。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM