如何处理时区问题而不将日期时间存储为TIMESTAMP WITH TIME ZONE

时间:2013-09-05 16:34:14

标签: c#-4.0 plsql wcf-data-services

我们正在开发一个移动应用程序,用于检索和显示旧系统中的数据。在旧系统中,日期时间列存储在日期数据类型中,而不是TIMESTAMP WITH TIME ZONE。现在我们需要在移动应用程序中处理时区问题。

我们希望数据的时间反映输入它的用户的时区(偏移到GMT)(或至少存储在GMT中),以便我们可以代表查看它的用户正确解释它。

我们的“极端情况”情况是时区A中的遗留系统用户输入时区A的订单,时区B中的数据库服务器以及时区C中的移动应用用户。

数据似乎没有偏移信息且不在GMT中,因此每个步骤都会“推断”时区:

  • 时区A中的用户看到自输入以来的“正确”时间 因为该时区正确解释。

  • 移动应用数据服务(持久化/序列化数据集) 从时区B中的数据库服务器读取数据并解释 在时区B的时间,时间由一些人关闭 小时数。

  • 时区C中的移动应用用户将时间解释为 时区C,时间已经过了几个小时。

如果我们输入了订单的时区信息,我们就可以决定如何显示它:

  • 就“发起人”时区而言

  • 就“观众”时区而言

如果我们在GMT中有一个绝对时间,我们就可以根据“观察者”时区显示它(不知道“发起者”时区)。

将遗留DATE数据类型更改为TIMESTAMP WITH TIME ZONE不是一个选项,因为它对我们来说太大了,可能还有其他复杂情况。

这是我的创可贴解决方案

  1. 创建包含UTC信息的位置表。

  2. 将一个loc_code列作为FK添加到订单表。

  3. 加入订单和位置表,并将order_date转换为TIMESTAMP WITH TIME ZONE。

  4. 我知道这个解决方案存在漏洞,我正在寻找更好的方法来实现我们的目标。任何意见都将不胜感激。

    以下是代码,

    DROP TABLE location;
    CREATE TABLE location
    (
      loc_code                    VARCHAR2(6)  NOT NULL,
      descr                       VARCHAR2(40) NOT NULL,
      utc_time_zone_offset        VARCHAR2(6)  NULL,
      daylight_savings_start_date DATE         NULL,
      daylight_savings_end_date   DATE         NULL,
      CONSTRAINT locationp1 PRIMARY KEY (loc_code)
    );
    
    INSERT INTO location VALUES ('-5','EST','-05.00',To_Date('2013-03-10 2:00:00','yyyy-mm-dd hh24:mi:ss'),To_Date('2013-11-03 2:00:00','yyyy-mm-dd hh24:mi:ss'));
    INSERT INTO location VALUES ('-6','CST','-06.00',To_Date('2013-03-10 2:00:00','yyyy-mm-dd hh24:mi:ss'),To_Date('2013-11-03 2:00:00','yyyy-mm-dd hh24:mi:ss'));
    INSERT INTO location VALUES ('-7','MST','-07.00',To_Date('2013-03-10 2:00:00','yyyy-mm-dd hh24:mi:ss'),To_Date('2013-11-03 2:00:00','yyyy-mm-dd hh24:mi:ss'));
    INSERT INTO location VALUES ('-8','PST','-08.00',To_Date('2013-03-10 2:00:00','yyyy-mm-dd hh24:mi:ss'),To_Date('2013-11-03 2:00:00','yyyy-mm-dd hh24:mi:ss'));
    INSERT INTO location VALUES ('-9','ALST','-09.00',To_Date('2013-03-10 2:00:00','yyyy-mm-dd hh24:mi:ss'),To_Date('2013-11-03 2:00:00','yyyy-mm-dd hh24:mi:ss'));
    INSERT INTO location VALUES ('-10','HST','-10.00',To_Date('2013-03-10 2:00:00','yyyy-mm-dd hh24:mi:ss'),To_Date('2013-11-03 2:00:00','yyyy-mm-dd hh24:mi:ss'));
    COMMIT;
    
    DROP TABLE orders
    CREATE TABLE orders (order_id VARCHAR2(10),loc_code VARCHAR2(6), order_date DATE);
    
    SELECT order_id,order_date,
           cs_timezone.to_UCT(loc_code,order_date),
           cs_timezone.to_viewer_time(loc_code,order_date)  
      FROM orders 
    
    CREATE OR REPLACE PACKAGE cs_timezone AUTHID CURRENT_USER AS
    -- 1. Only SYSDATE is affected by SYSTIMESTAMP which the timestamp on the server machine itself.
    --    If we do not use SYSDATE, we do not have to worry about SYSTIMESTAMP.
    -- 2. When insert a DATE value Oracle only stores the date time value without any knowledge of time zone.
    --    When we convert to local time we only care about time zone offset for the user who save the data.
    
      FUNCTION to_viewer_time (p_loc_code IN VARCHAR2, p_date IN DATE) RETURN DATE;
      FUNCTION to_UCT (p_loc_code IN VARCHAR2, p_date IN DATE) RETURN TIMESTAMP WITH TIME ZONE;
    END;
    /
    
    CREATE OR REPLACE PACKAGE BODY cs_timezone
    AS
    FUNCTION to_viewer_time (p_loc_code IN VARCHAR2, p_date IN DATE) RETURN DATE
    IS
      v_offset_hrs INT;  v_utc_time_zone_offset VARCHAR2(10); v_daylight_savings_start_date DATE; v_daylight_savings_end_date DATE;
    BEGIN
      SELECT   utc_time_zone_offset,   daylight_savings_start_date,   daylight_savings_end_date
        INTO v_utc_time_zone_offset, v_daylight_savings_start_date, v_daylight_savings_end_date
        FROM location
       WHERE loc_code = p_loc_code;
      IF InStr(v_utc_time_zone_offset,'+') > 0 THEN
        IF To_Date(To_Char(p_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') BETWEEN To_Date(To_Char(v_daylight_savings_start_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') AND To_Date(To_Char(v_daylight_savings_end_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') THEN
           v_utc_time_zone_offset := SubStr(v_utc_time_zone_offset,1,1)||To_Char(To_Number(v_utc_time_zone_offset)+1);
        END IF;
        v_offset_hrs :=  extract(TIMEZONE_HOUR FROM current_timestamp) - To_Number(v_utc_time_zone_offset);
      ELSIF InStr(v_utc_time_zone_offset,'-') > 0 THEN
        IF  To_Date(To_Char(p_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') BETWEEN To_Date(To_Char(v_daylight_savings_start_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') AND To_Date(To_Char(v_daylight_savings_end_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') THEN
           v_utc_time_zone_offset := To_Char(To_Number(v_utc_time_zone_offset)+1);
        END IF;
        v_offset_hrs := To_Number(v_utc_time_zone_offset) - extract(TIMEZONE_HOUR FROM current_timestamp);
      ELSE
        Raise_Application_Error(-20001,'Offset format missing - or +');
      END IF;
      Dbms_Output.put_line(v_offset_hrs); 
      RETURN p_date+(v_offset_hrs/24);
    EXCEPTION WHEN OTHERS THEN RETURN 'ERROR: '||SQLERRM;
    END to_viewer_time;
    
    FUNCTION to_UCT (p_loc_code IN VARCHAR2, p_date IN DATE) RETURN TIMESTAMP WITH TIME ZONE
    IS
      v_utc_time_zone_offset VARCHAR2(10); v_daylight_savings_start_date DATE; v_daylight_savings_end_date DATE;
    BEGIN
      SELECT   utc_time_zone_offset,   daylight_savings_start_date,   daylight_savings_end_date
        INTO v_utc_time_zone_offset, v_daylight_savings_start_date, v_daylight_savings_end_date
        FROM location
       WHERE loc_code = p_loc_code;
      IF InStr(v_utc_time_zone_offset,'+') > 0 THEN
        IF To_Date(To_Char(p_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') BETWEEN To_Date(To_Char(v_daylight_savings_start_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') AND To_Date(To_Char(v_daylight_savings_end_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') THEN
          v_utc_time_zone_offset := SubStr(v_utc_time_zone_offset,1,1)||To_Char(To_Number(v_utc_time_zone_offset)+1);
          IF InStr(v_utc_time_zone_offset,'-') = 0 OR InStr(v_utc_time_zone_offset,'+') = 0 THEN
            v_utc_time_zone_offset := '+'||v_utc_time_zone_offset;
          END IF;
          IF InStr(v_utc_time_zone_offset,'.00') = 0 THEN
            v_utc_time_zone_offset := v_utc_time_zone_offset||':00';
          ELSE
            v_utc_time_zone_offset := REPLACE(v_utc_time_zone_offset,'.',':');
          END IF;
        ELSE
          v_utc_time_zone_offset := REPLACE(v_utc_time_zone_offset,'.',':');
        END IF;
      ELSIF InStr(v_utc_time_zone_offset,'-') > 0 THEN
        IF To_Date(To_Char(p_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') BETWEEN To_Date(To_Char(v_daylight_savings_start_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') AND To_Date(To_Char(v_daylight_savings_end_date,'mm-dd hh24:mi:ss'),'mm-dd hh24:mi:ss') THEN
          v_utc_time_zone_offset := To_Char(To_Number(v_utc_time_zone_offset)+1);
          IF InStr(v_utc_time_zone_offset,'-') = 0 OR InStr(v_utc_time_zone_offset,'+') = 0 THEN
            v_utc_time_zone_offset := '+'||v_utc_time_zone_offset;
          END IF;
          IF InStr(v_utc_time_zone_offset,'.00') = 0 THEN
            v_utc_time_zone_offset := v_utc_time_zone_offset||':00';
          ELSE
            v_utc_time_zone_offset := REPLACE(v_utc_time_zone_offset,'.',':');
          END IF;
        ELSE
          v_utc_time_zone_offset := REPLACE(v_utc_time_zone_offset,'.',':');
        END IF;
      ELSE
        Raise_Application_Error(-20001,'Offset format missing - or +');
      END IF;
      Dbms_Output.put_line(''); 
      RETURN TO_TIMESTAMP_TZ(To_Char(p_date,'YYYY-MM-DD HH24:MI:SS')||' '||v_utc_time_zone_offset, 'YYYY-MM-DD HH24:MI:SS TZH:TZM');
    EXCEPTION WHEN OTHERS THEN RETURN 'ERROR: '||SQLERRM;
    END to_UCT;
    
    END cs_timezone;
    

    谢谢,

    肖恩

1 个答案:

答案 0 :(得分:0)

如果您拥有时区的名称,则可以使用类似于以下内容的查询来调整更改时间戳:

SELECT
  e.last_updated_date AS cst_last_updated_date,
  (FROM_TZ(e.last_updated_date, 'US/Central') AT TIME ZONE 'US/Eastern') AS est_last_updated_date
FROM events_tbl e;

根据我的经验,这将根据区域设置自动计算DST。

如果您正在使用DATES而不是TIMESTAMPS,您还可以执行以下操作:

select 
  CAST (e.last_updated_date AS TIMESTAMP) AS cst_last_updated_date,
  (FROM_TZ(CAST (e.last_updated_date AS TIMESTAMP), 'US/Central') AT TIME ZONE 'US/Eastern') AS est_last_updated_date
from events_tbl e;