2012年2月14日 星期二

GWT、GAE 的日期問題

開發環境:GWT 2.4.0、GAE 1.6.1、GPE 2.5.1、Objectify 3.0

正在作一個預約系統,很自然會有「取出某天所有預約」之類的需求,所以將 class 設計成這樣,期望能節省一些麻煩:
class Booking{
    Date date;
    int hour;  //時間間隔以「小時」為單位
    //skip other field
}

由於 GAE 的 Datastore 理論上沒有提供在 query 時可以卡入日期函數,所以如果不對日期作處理,下面這段「取得今天的預約」會完全取不到任何東西:
Objectify ofy = ObjectifyService.begin();
ofy.query(Booking.class).filter("date", new Date()).list();  //empty list

原因應該是在於 Datastore 作比對時是直接使用 equal()(個人猜測,沒有實際證據),而 java.util.Date 的 equal() 是判斷 getTime()...... 這取得到東西的或然率真的接近零了。

最直覺的反應是將日期中「小時」以下的單位全部歸零,在 GWT 的 CalendarUtil 可以找到 resetTime() 的寫法:
@SuppressWarnings("deprecation") // GWT requires Date
private static void resetTime(Date date) {
    long msec = date.getTime();
    msec = (msec / 1000) * 1000;
    date.setTime(msec);

    // Daylight savings time occurs at midnight in some time zones, so we reset
    // the time to noon instead.
    date.setHours(12);
    date.setMinutes(0);
    date.setSeconds(0);
}

用這個方法,將 client 端傳上 server 的 date 都先「整理」過。當然,媽媽說:「不要總是相信 client 端給的值」,抱持著能省則省的白痴想法,所以忽略 deprecated 的訊息,讓 server 也用同樣方法...... 神奇的事情發生了,有時候會正確、有時候會失敗......
太銷魂了,讓我想要貼張圖來宣洩一下澎湃的情緒

問題可以簡化成「client 端傳遞一個 reset 過的 date 給 server、server 再 reset 一次回傳回來」,把兩個 date 分別印出來,在 GPE 的 Development Mode 中,有時候會得到這樣的結果:
Sat Feb 11 12:00:00 CST 2012
Sat Feb 11 20:00:00 CST 2012

整整差了八小時...... 是的,是時區的問題。如果 server 端在 reset 之前先把 date 印出來,會得到「Sat Feb 11 04:00:00 UTC 2012」。server 用 UTC 時區,這很合情合理,實際 deploy 上 AppEngine 也是同樣的結果,完全就是自己不應該硬要用 deprecated 的東西。但那「有時候會正確、有時候會失敗」的靈異事件又是怎樣?事實的真相是:
在一開始啟動 Development Mode 的 server 時,server 的時區是 UTC(所以會失敗);但是在 reload web server 之後,server 的時區就會變成本地時區(所以會成功)。
這大概是最近發現第三個 GPE 的不思議之謎(第一個是「更改 gwt.xml 需 refresh 兩次才見效」、第二個是「build project 進度會卡住,關掉網路就沒事」)

好了,就讓系統一個日期各自表述,server 改用 Calendar 來設定日期,目前看起來運作正常...... [抖]
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT+800"));
c.setTime(new Date((date.getTime()/1000)*1000));
c.set(Calendar.HOUR_OF_DAY, 12);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
return c.getTime();

以下是延伸閱讀時間 XD:
  • 在 objectify-appengine 的 Google Group,Jeff Schnitzer 提到用 java.sql.Date。雖然說 Objectify 2.2.2 版就可以處理 java.sql.Date(GAE 只能儲存 java.util.Date)、GWT 也能使用 java.sql.Date,不過不知道是我誤會意思還是?實測的結果還是得先 reset 過才能符合需求——而且 java.sql.Date 基本上不能設定時間...
  • tkcn 提供的 reference(12)提到可以用「Joda-Time」這個 library 來處理、以及一些其他資訊。但是目前安逸於現在這個解法...... [毆飛]
  • 原始 G+ 討論串

結語:這是一篇看起來好像很複雜,實際上根本就是技術太弱才會搞出來的飛機......

6 則留言:

  1. gwt現在資源真少 希望原PO可以多分享一些XD

    回覆刪除
  2. How about using filter("date >", date1).filter("date <", date2), while date1 is the start of the date and date2 is the end of date?

    回覆刪除
  3. I don't understand what you(Inspire) mean...... Orz

    If you are talking about "get all booking record in the same day", there is an obvious performance issue : system will filter two times. On the other hand, you must make another Date value... Is it worth?

    回覆刪除
  4. Yes, performance is the key, thanks for the input.
    However i'm still not clear about your design: do you have to reset the date field to a specific value(say, 12:00:00) in a day when saving the entity(Booking), in order to make it filterable using:
    ofy.query(Booking.class).filter("date", SPECIFIC_DATE).list();
    right?

    If so, what if the end user want to see the exact time he made the booking?
    Thanks.

    回覆刪除
  5. At the beginning of the article:
    正在作一個預約系統,很自然會有「取出某天所有預約」之類的需求,所以將 class 設計成這樣,期望能節省一些麻煩:
    class Booking{
    Date date;
    int hour; //時間間隔以「小時」為單位
    //skip other field
    }

    I use another field "hour" to save actual booking time. (Yes, in the customer's requirement, the minimal time unit is hour)

    回覆刪除