簡介
本文展示了一個 DB2 用戶定義函數(UDF)的 Java 實現的設計,該函數有兩個輸入參數:一個 UTC 時間戳(例如 2004-04-04-04.00.00.000000)和一個地區名(例如 "America/Guayaquil"),並返回新地區中對應的時間戳(在這裡是 2004-04-03-23.00.00.000000)。
執行這種轉換時遇到的兩個主要挑戰是:
對於包含來自很多不同應用程序和分布在幾個洲的地區的數據的數據倉庫項目,經驗證明,在轉換方面,該函數對於 Extract Transform Load (ETL) 在分析和處理數據時需要用到的過程極其有用。
背景
時間連續區間(time continuum)內的某個惟一時刻可以由那一刻的 日期、 時間來定義,為了更精確起見,還可以加上 秒後面的小數。這種定義叫做 時間戳,它在 DB2 中的 ISO 表示如下:
2004-07-24-16.18.28.410002
在這種情況下,精確度可以下調為 1 微秒。
在一個與某個事務有關的事件發生時記錄下時間戳,這一活動就叫做為該事務“記錄時間戳(timestamping)”。在一個事務的整個生命周期內,需要多次記錄時間戳,以便記下創建、最後一次修改、最後一次訪問某一事務的時間。
對於發生在多個地理位置和多個地區的集中存儲事務,常見的設計是用與該地區時間對應的世界協調時間(Universal Time Coordinated,UTC)記下每個時間戳記錄。通過同時記錄下事務發生時所在的位置,例如存儲事務發生時所在的客戶號或業務部門號,可以重新構造該事務的本地時間。
當必須處理、分析和報告來自大量不同來源的數據時,我們需要重新構造 UTC 時間戳在事務原始位置的本地時間。常被問到的問題有:
挑戰
表面上看起來既簡單又容易完成的任務,事實上被證明是既不簡單也不容易完成。
從本地時間到 UTC 時間的實時轉換通常是在應用程序中完成的。它只能用於當前時間(而不能用於過去或將來的任何時間點),並且只能用於應用程序運行時其服務器所在的地區(而不是任何其他地區)。
我們遇到的一個挑戰是理解我們所接收到的地區名:
另一個挑戰是實現我們處理的所有地區的夏令時(Daylight Savings Time,DST)規則 —— 這些規則我們還不能輕松地使用。
還有一個需求就是能夠很容易地使用用來調用轉換函數的 API。
例如,為了得到一個 UTC 時間戳在 "America/Nassau" 的本地時間,一家虛構的 Acme Intl. 公司使用的 SELECT 語句就必須像下面這樣簡單:
SELECT ACME.F_CONVERT_TIMEZONE(TRANSACTION_TIMESTAMP, "America/Nassau")FROM ACME.TRANSACTION_TABLE;
解決方案
有些數據源是由 Java 應用程序填充的,因此每個應用程序用於這些數據源的名稱可以是不同 Java JDK 版本(1.1.8、1.4 等)的名稱。由於 Java JDK 已經為一組相當全面的地區的所有 DST 規則編寫了代碼,因此我們選擇用 Java 編寫轉換函數,並在 DB2 的 Java JDK 上運行該函數。
為了便於使用,我們將 Java 類包裝在了另一個 DB2 UDF 中。
解決方案細節
Java 類有一個用來存儲查找字典的類變量,其中包含了可能作為輸入的每個地區所有可能的拼寫和命名。
tz_map = new Hashtable();… tz_map.put("Eastern Daylight", "EST");tz_map.put("Eastern Standard Time", "EST");tz_map.put("America/New_York", "EST");…
例如,上面所有的鍵都對應於值 “EST"。這就是類方法為了進行時間戳轉換而在內部使用的值。
注意:這裡鼓勵為時區使用長名,例如 "America/New_York"。但是在這個特定的實現中,我們使用了短名稱,因為 UDB DB2 version 7.2 使用的是 JDK 1.1.8,該版本只能使用短名稱。
查找表的填充是手動完成的。我們花了很大的精力查找每個地區的內部 Java 設置,並將具有相同 DST 規則和時區的長名與短名進行組對。
我們這樣映射了 250 多個地區。如果需要的話,還可以添加新的映射。這樣,在將新數據源與新的地區一起添加到數據倉庫時,我們便有了所需的靈活性。
對於實際的時間戳轉換,我們使用了下面的類方法:
public static java.lang.String J_CONVERT_TIMEZONE(java.lang.String ivc_UTCtimestamp, Java.lang.String ivc_timezone)
首先將輸入的時間戳字符串解析成它的各個組成部分,並從那些值例化出一個 Java 日歷,然後通過格式轉換器(formatter)產生一個新的轉換後的時間戳。細微部分沒有進行轉換,直接變成輸出,因為 Java Calendar 沒有精確到那個程度。
可以用下面的語句將該 Java 類方法注冊成一個 UDF 函數:
public static java.lang.String J_CONVERT_TIMEZONE(java.lang.String ivc_UTCtimestamp, Java.lang.String ivc_timezone) throws Exception { // get the short name equivalent of the input ivc_timezone = (String)tz_map.get(ivc_timezone); if (ivc_timezone == null) ivc_timezone = "GMT"; // default to UTC if entry not found // replace the . with - so that we only have one token separator instead of two String ivc_UTCtimestamp_new = ivc_UTCtimestamp.replace('.', '-' ); // parse, validate and convert the TS string to integers, based on the one separator StringTokenizer st = new StringTokenizer(ivc_UTCtimestamp_new, "-"); int year = Integer.parseInt(st.nextToken()); int month = Integer.parseInt(st.nextToken()); int day = Integer.parseInt(st.nextToken()); int hour = Integer.parseInt(st.nextToken()); int min = Integer.parseInt(st.nextToken()); int sec = Integer.parseInt(st.nextToken()); String micro = st.nextToken(); // just carried over from the input // create with the above a calendar in UTC Calendar calUTC = Calendar.getInstance(); calUTC.clear(); calUTC.setTimeZone(TimeZone.getTimeZone("GMT")); calUTC.set(year, month-1, day, hour, min, sec ); // prepare the formatter for the specifIEd timezone DateFormat formatter = new SimpleDateFormat("yyyy'-'MM'-'dd'-'HH.mm.ss", Locale.US); TimeZone tz = TimeZone.getTimeZone(ivc_timezone);