程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 淺析J2EE應用中的時間值字段的數據類

淺析J2EE應用中的時間值字段的數據類

編輯:關於JAVA

概述

在設計庫表時,經常會碰到用於保存"時間值"的字段,如create_date,begin_time,login_time等,舉不勝舉。針對這些類型的字段,在設置數據類型時,有一個有趣的現象,即其中一些人使用Date類型,而另外一些人使用Char(8)/Char(14)類型。一般而言,初學者,在校學生,甚至老師一般都屬於前者,他們一個鮮明的特征是對數據庫的理論掌握很好,但普遍缺少實際項目的開發經驗;而後者一般是那些有一定項目經驗的開發人員。乍一看,這些時間值字段,用Date類型應該是合情合理,天經地義的,為什麼有一定項目經驗的人偏偏要這樣"棄暗投明",這樣"特立獨行"呢?

這是典型的白貓黑貓問題,理論化的東西很光鮮,但有時在實踐中就是不靈光,而一些"旁門左道"的東西卻顯得更加方便易用。本文將通過一個具體例子的不同開發過程,分析Char類型時間字段為什麼在實踐中更受歡迎。考慮到篇幅所限,我們僅對Date類型和Char(8)類型的時間值字段作比較分析,對於Date類型和Char(14)類型的分析,相信大家完全可以由此而及彼。

1、比較的例子

我們設計了一個具體的實例,對用Char類型和Date類型的日期進行比較分析,使用的是Oracle數據庫,現對該實例進行簡單的描述。

假設有一個T_USER表,有一個EXPIRE_DATE(過期日期)字段,要求記錄年、月、日的日期數據,對EXPIRE_DATE字段分別采用兩種實現方式:

圖 1 T_USER表

左邊的T_USER(1)使用CHAR(8)保存日期值,以yyyymmdd格式保存,如20070606,20070501;而右邊的T_USER(2)使用Date數據類型,我們稱左邊的數據表設計為CHAR類型日期方案,而右邊的設計為DATE類型日期方案。

表中的數據當然不會生而有之,我們假設從Web的表單上提交上來,保存到表中,當然還要有查詢、統計等操作,我們就通過這些常見的數據操作分析這兩個方案的不同,通過這樣的分析,孰劣孰優,相信我們就可以進行很好的判斷了。

2、從表單添加記錄的比較

Struts+Spring+Hibernate是目前Web項目中流行的框架,在這個框架中,Hibernate需要為T_USER生成一個User.java的PO,CHAR類型日期方案的User.expireDate為String類型,而DATE類型日期方案的User.expireDate為java.sql.Date類型,如圖 2所示:

圖 2 兩方案分別對應的User.java PO

而對應Struts的展現層,需要提供一個UserActionForm,以獲取頁面表單的提交數據。不管采用哪種日期方案,UserActionForm.expireDate屬性類型均為String,因為這樣一來,可以直接從Struts的<html:text property="expireDate"/>獲取數據,另外也方便數據回顯到頁面中;如果UserActionForm.expireDate采用java.sql.Date類型,則<html:text property="expireDate"/>標簽的數據將無法正確地填充UserActionForm.expireDate對象屬性中。

圖 3 UserActionForm.java

表單提交上來的expireDate是帶時間格式的字符串,如2006-06-06,2001-10-12,UserActionForm.expireDate簡單地接受該值,在UserAction中,必須用UserActionForm的數據生成持久層所需的PO,即User對象。兩種日期方案在數據的轉換邏輯的區別分別描述如下:

CHAR類型日期

由於User.expireDate也是String類型,因此,僅需要將UserActionForm.expireDate屬性完全拷貝到User中,然後再將User.expireDate屬性的日期格式符"-"去除卻可,卻將2006-06-06轉換為20060606,對應操作邏輯的主要代碼如下:

1. User user = new User();

2. //將userActionForm中的數據拷貝到user對象中

3. BeanUtils.copyProperties(user, userActionForm);

4. //將日期格式符去除,得到數據庫存儲日期格式,如將2006-06-06轉換為20060606

5. user.setExpireDate(user.getExpireDate().replace("-",""));

6. …

7. //調用服務對象,將user保存到T_USER中

8. userService.save(user);

·DATE類型日期

在DATE類型日期方案中,由於PO User.expireDate屬性為java.sql.Date,和UserActionForm.expireDate 存在類型的不匹配,因此需要通過一個轉換函數,將String日期轉換為java.sql.Date的日期。其主要代碼如下:

1. User user = new User();

2. //由於expireDate不能直接進行拷貝,因此需要逐一拷貝屬性

3. BeanUtils.copyPropertie(user, userActionForm,"userId");

4. BeanUtils.copyPropertie(user, userActionForm,"userName");

5. //使用轉換函數str2Date()將String類型的時間轉換為java.sql.Date的時間

6. java.sql.Date expireDate = str2Date(userActionForm.getExpireDate());

7. //設置expireDate屬性

8. user.setExpireDate(expireDate);

9. …

10. //調用服務對象,將user保存到T_USER中

11. userService.save(user);

通過上面的比較,可以看出,使用DATE時間方案比使用CHAR時間方案在添加數據的處理上要復雜一些,表現在:

1) 由於屬性名相同而類型存在不可直接轉換的問題將導致無法進行對象間屬性批量拷貝,即BeanUtils. copyProperties()批量屬性拷貝函數會拋出異常,因此只能手工逐一進行單個具體屬性的拷貝,如果屬性個數很多,這一機械式的屬性拷貝代碼塊就要相應增大,不但使代碼顯示臃腫難看,而且直接降低了代碼的可維護性,因為一但因表字段名改變,就需要手工調整這段代碼。

2) 需要提供一個將String日期串轉換為java.sql.Date的轉換函數,將年、月、日時間域分別從字串中抽取出來,並轉換為int類型,然後利用java.sql.Date(int year,int month,int date)構造函數得到對應的java.sql.Date對象。

3、在數據查詢上的比較

假設需要以EXPIRE_DATE字段為條件查詢T_USER的記錄,由於已經在T_USER.EXPIRE_DATE字段上建立了索引,在查詢時需要考慮使用該索引。Web的查詢界面如下:

圖 4 查詢界面

日期條件值可以是yyyy、yyyy-mm、yyyy-mm-dd的格式,假如開始日期為2001,結束日期為2002,則表示日期區間為2001-01-01到2002-12-31,如果開始日期為2001,結束日期為2002-02,則表示日期區別為2001-01-01到2002-02-28,以此類推。此外,如果開始日期條件未提供,表示查詢所有小於等於結束日期的記錄,反之如果結束日期條件未提供,表示查詢所有大於等於開始日期的記錄。

·CHAR類型日期

CHAR類型日期數據表保存的是CHAR(8)類型的日期,此時可以用簡單的方法構造出查詢語義豐富,語句結構統一的查詢SQL語句,構造方法如下:

1) 去除格式符:將開始,結束查詢日期值中的時間格式符去除。

2) 補尾串:將開始日期字符串末尾用0補齊到8位長度,將結束日期字符串末尾用9補齊到8位長度。特別的,如果開始日期為空,則用00000000代替,而結束日期未提供則用99999999代替。

3) 構造查詢SQL:用以下SQL語句構造查詢語句:

select * from T_USER where EXPIRE_DATE between <開始日期> and <結束日期>

表 1 CHAR類型日期查詢SQL結構

舉個例子:假如開始,結束日期值分別為2001、2002-02,則按以上三步的處理過程分別為:

1)去除格式符:2001,200202

2)補尾串:20010000,20020299

3)構造查詢SQL:

select * from T_USER where EXPIRE_DATE between '20010000'and '20020299'

又如開始日期為空,結束日期為200203,則對應的查詢SQL為:

select * from T_USER where EXPIRE_DATE between '00000000'and '20020399'

·DATE類型日期

由於DATE類型日期在數據庫表中對應的是Date類型字段,首先,我們不能仿照CHAR類型日期的查詢SQL結構構造如下的查詢SQL:

select * from T_USER where to_char(EXPIRE_DATE,'yyyymmdd') between <開始日期> and <結束日期>

因為在EXPIRE_DATE上建立了索引,如果在EXPIRE_DATE施加了to_char()的數據庫函數,就無法使用該索引,將引發一個全表描述。

所以,還得將開始、結束日期字符串用to_date()數據庫函數轉換為Date類型,如:

select * from T_USER where EXPIRE_DATE between to_date(<開始日期>,'yyyymmdd') and to_date(<結束日期>,'yyyymmdd')

表 2 CHAR類型日期查詢SQL結構

但由於使用了to_date字符串日期轉換函數,就必須保證開始日期和結束日期的字符串必須是語義合法日期字符串,如20010101,20020228,如果是語義錯誤的日期字符串,如20010000,20020299,to_date函數將發生轉換錯誤,致使上面的查詢SQL語句運行錯誤。因此,只有開始日期和結束日期字符串都合法時,才可以使用上式的查詢SQL。

如果開始或結束日期未精確到日,即只精通到年或月,如2001,200202,則在應用程序的服務層,必須對日期串進行語義分析,將其補齊到8位合法日期字符串,如開始日期字符串"2001"就必須補齊為"20010101",而結束日期字符串"200202"就必須先補齊為"20020228"(非潤年的平月),而這一轉換邏輯處理起來是比較費神費力的,一不小心就可能引入一個Bug。

第二個麻煩的問題是,如果開始日期和結束日期為空,SQL語句又該如何構造呢?如果還按照表 2的SQL結構進行構造,那麼就必須回答一個問題:最小開始日期和最大的結束日期分別是多少,因為你不能用"00000000"來代表最小的日期,也不能用"99999999"來代表最大的日期。

為了避免回答這個問題,就需要在開始日期和結束日期為空時分別采用不同結構的查詢SQL語句:

select * from T_USER where EXPIRE_DATE >= to_date(<開始日期>,'yyyymmdd') 表 3 結束日期條件值為空時

select * from T_USER where EXPIRE_DATE <= to_date(<結束日期>,'yyyymmdd') 表 4 開始日期條件值為空時

綜上所述,為了使用在EXPIRE_DATE字段上的索引,DATE類型日期在構造查詢SQL上明顯比CHAR類型日期復雜,具體表現在以下兩點:

1) 需要對日期條件值進行語義分析,以得到精確到日的語義合法的日期字符串。

2) 需要為開始/結束日期條件值均不為空,開始日期條件值為空,結束日期條件值為空三種情況分別構造不同結構的SQL語句,也構造SQL的程序必須對應一個分支。

5、在數據庫移植上的比較

由於CHAR類型日期實際上是一個字符類型字段,字符類型是最基本的數據類型,在構造Insert ,Update,Delete,Selete的SQL時,各種數據庫對字符類型字段的處理幾乎一致,因此在數據庫的移植上比較容易。

對於DATE類型的日期,由於不同數據庫對日期的操作差異很大,如獲取數據庫的時間函數,Oracle為sysdate,SqlServer為getdate(),而MySql為now();從Date字段中抽取年的數值,Oracle為extract(year from <date>),SqlServer和MySql均為month(<date>)。由於日期函數在不同數據庫差別巨大,帶DATE類型日期字段的表在數據庫的移植上就不如CHAR類型日期來得簡單易行。

也許,有人會說現在都采用Hibernate進行映射ORM了,Hibernate已經屏蔽了具體數據庫的不同,何來的數據庫移植?這話在一定程度上是沒有錯的,但是Hibernate框架由於通過對象映射的方法產生SQL語句,有時往往很難獲得最優的查詢性能的SQL語句。所以,對於一些有性能要求較高的查詢,往往采用直接編寫SQL語句,或采用iBatis框架,後兩者都需要直接使用SQL語句,此時數據庫的移植問題就暴露出來了。

不但在數據庫的移植問題上,CHAR類型日期比DATE類型日期擁有絕對的優勢,在數據的導入/導出,數據傳輸等方面,CHAR類型日期比DATE類型日期也具有較多的優勢。字符型的數據可以直接不失真地用文本或XML表示,而Date類型導出為文本時,如果不指定好轉換格式往往難於處理,如2001-01-01的Date數據在轉換為文本時,可能變為1st Mon 2001,也可能為2001-01-01 00:00:00 ,甚至可能是01-01-01。這樣,在導入時顯得難以操作,因為導入/導出都需要指定好日期格式。

6、總結

有一句很經典的關於軟件設計的話:如果你的程序邏輯變得很復雜,也許並不是問題域本身的復雜度造成的,往往將歸因於設計上的缺陷和瑕疵。同樣一個問題,采用不同的策略,往往造成大相徑庭的解決復雜度。Spring框架功能強大,我本以為代碼一定很復雜,但是當我研讀了Spring框架的代碼時,才詫異地發現Spring框架的源碼很少有超過300行代碼的類,類和方法大多簡潔明了,真是應了那句大巧若拙,大道至簡的話了。

在庫表設計時,日期字段究竟是采用CHAR類型日期還是DATE類型日期,在作出選擇時,需要考慮在程序邏輯中該數據的加工操作邏輯,畢竟表字段是需要在程序邏輯中使用和操作的,而非僅僅做一個簡單的記錄而已。

通過上文的分析,我們發現CHAR類型日期可以在較大的程度上簡化程序的開發,並且充分利用索引,獲取較好的性能。但又一個問題產生了,既然CHAR類型日期這麼不好用,各數據庫又都提供了Date日期格式,是否Date數據類型就成了屍位素餐的擺設了呢?換言之,Date數據類型適合在什麼場合使用呢?筆者個人的建議是,在幾乎任何時候都不要使用Date類型作為表字段類型,Date類型僅在存儲過程,數據庫函數這些數據庫程序邏輯代碼編寫場合使用,即把它看成是一個運算過程的中間工具而不要用其作數據的存儲形式。

另外還有一個需要探討的問題,那就是Date類型長度是7,而Char(8)或Char(14)要比之浪費不少的存儲空間,其中這個問題在古代確實是一個大問題,那時候一間茅屋都要合理利用,現在很容易就可以得到廣廈千萬間。由於硬件性價比持續提升,也就可以使我們采用一些軟件上更簡便的方法來改善程序的設計,如Java的代碼反射,IoC的實現注入,XML形式的數據表示都是受惠於硬件的提升。因此,現在,一般而言,很微小的存儲空間占用和性能的影響並不需要設計人員特別的關注,他們要更多關注的往往是如何使邏輯簡化,如何使系統更具擴展性,可維護性和移植性上。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved