0

我正在尝试使用休眠在 Oracle 中保存两个日期。两个日期在莫斯科时区具有相同的时间戳:2005-10-30T02:00+03:00[Europe/Moscow] 和 2005-10-30T02:00+04:00[Europe/Moscow](“Sun Oct 30 02:00:00 MSK 2005”和“Sun Oct 30 02:00:00 MSD 2005”)。这些日期在时间上相隔一小时,并且与冬/夏时间的转换有关。

我在 Oracle 中创建了表:

create table TMP
(
    ID    LONG,
    TS    TIMESTAMP,
    TSLTZ TIMESTAMP WITH LOCAL TIME ZONE,
    TSTZ  TIMESTAMP WITH TIME ZONE
);

和我模块中的实体:

@Entity
@Table(name = "tmp")
public class DateTimeOracle {

    private Long id;
    private ZonedDateTime ts;
    private ZonedDateTime tsltz;
    private ZonedDateTime tstz;

    @Id
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public ZonedDateTime getTs() {
        return ts;
    }

    public ZonedDateTime setTs(ZonedDateTime ts) {
        this.ts = ts;
    }

    public ZonedDateTime getTsltz() {
        return tsltz;
    }

    public ZonedDateTime setTsltz(ZonedDateTime tsltz) {
        this.tsltz = tsltz;
    }

    public ZonedDateTime getTstz() {
        return tstz;
    }

    public ZonedDateTime setTstz1(ZonedDateTime tstz) {
        this.tstz = tstz;
    }
}

在实体中,所有字段都由单个日期初始化。保存后,Oracle 中的两个日期具有相同的值,如下所示:

ts = 2005-10-30 02:00:00.000000

TSLTZ = 2005-10-29 23:00:00.000000

TSTZ = 2005-10-30 02:00:00.000000 +04:00

为什么 oracle 对不同的日期(包括偏移量 +04:00)保持相同的值?有没有什么办法解决这一问题?

PS Postgres 正确存储日期。一个偏移量为 +03:00,另一个偏移量为 +04:00(分别为 2005-10-29 23:00:00.000000 和 2005-10-29 22:00:00.000000)。

更新

这就是我创建日期的方式:

Date dt2 = new Date(1130623200000L); //2005-10-29 23:00:00 +04:00
Date dt3 = new Date(1130626800000L); //2005-10-29 23:00:00 +03:00
ZonedDateTime zdt2 = ZonedDateTime.ofInstant(dt2.toInstant(), ZoneId.systemDefault()); // My zone is MSK
ZonedDateTime zdt3 = ZonedDateTime.ofInstant(dt3.toInstant(), ZoneId.systemDefault()); // My zone is MSK
OffsetDateTime odt2 = zdt2.toOffsetDateTime();
OffsetDateTime odt3 = zdt3.toOffsetDateTime();

如果我不使用 Hibernate 并直接使用 jdbc,情况不会改变。

Connection conn = DriverManager.getConnection("<oracle_url>",
                        "<username>", "<password>");
PreparedStatement pstmt = conn.prepareStatement("insert into tmp (id, TSTZ1, TSTZ2) values (200, ?, ?)", Statement.RETURN_GENERATED_KEYS);
pstmt.setDate(1, new java.sql.Date(dt2.getTime()));
pstmt.setDate(2, new java.sql.Date(dt3.getTime()));
int z1 = pstmt.executeUpdate();
pstmt.close();
conn.close();

更新2

如果我通过 jdbc 驱动程序将 OffsetDateTime 保存在带有本地时区的时间戳或带有时区的时间戳中,那么一切都很好。

PreparedStatement pstmt = conn.prepareStatement("insert into tmp (TSLTZ1, TSLTZ2, TSTZ1, TSTZ2) values (?, ?, ?, ?)");    
pstmt.setObject(1, odt2);
pstmt.setObject(2, odt3);
pstmt.setObject(3, odt2);
pstmt.setObject(4, odt3);

我在数据库中看到:

2005-10-29 22:00:00.000000  2005-10-29 23:00:00.000000  2005-10-30 02:00:00.000000 +04:00   2005-10-30 02:00:00.000000 +03:00

但是,如果我保存 ZonedDateTime,则带有本地时区的时间戳中的值是正确的,但带有时区的时间戳中的值不正确。

PreparedStatement pstmt = conn.prepareStatement("insert into tmp (TSLTZ1, TSLTZ2, TSTZ1, TSTZ2) values (?, ?, ?, ?)");    
pstmt.setObject(1, zdt2);
pstmt.setObject(2, zdt3);
pstmt.setObject(3, zdt2);
pstmt.setObject(4, zdt3);

在数据库中我看到:

2005-10-29 22:00:00.000000  2005-10-29 23:00:00.000000  2005-10-30 02:00:00.000000 +04:00   2005-10-30 02:00:00.000000 +04:00

最后两个值不正确。

4

2 回答 2

2

关于 Oracle TIMESTAMP 数据类型的一些解释:

  • TIMESTAMP:不存储任何时区信息。如果您输入带有时区的时间戳,则时区信息将被截断并丢失。

  • TIMESTAMP WITH TIME ZONE:在您将时间戳插入数据库时​​,将时间戳与时区信息(即作为命名区域或作为 UTC-Offset)一起存储。

  • TIMESTAMP WITH LOCAL TIME ZONE:时间戳存储为DBTIMEZONE(推荐和通常UTC)。时间戳始终且仅显示在当前用户会话中SESSIONTIMEZONE。因此它不显示任何时区信息,因为根据定义,这始终是您的本地时区。

我应该使用哪一个?

这取决于您的要求 - 当然。

由于TIMESTAMP WITH LOCAL TIME ZONE您不必关心客户端的任何设置,时间始终显示为当地时间。时间存储在 中DBTIMEZONE,因此您丢失了原始插入的时区。

请注意,当您在TIMESTAMP WITH TIME ZONE. 无法直接在此类列上创建索引。相反,Oracle 会为该虚拟列创建一个虚拟列SYS_EXTRACT_UTC(TSTZ)并在该列上创建索引。开发查询时应注意这一点。

更新

你的情况很特殊。当您插入TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow'时,这个时间是模棱两可的,它可能意味着2005-10-30 02:00:00+03:002005-10-30 02:00:00+04:00

举个例子:

SELECT TO_CHAR(TIMESTAMP '2005-10-30 00:00:00 Europe/Moscow' + LEVEL * INTERVAL '1' HOUR, 
    'YYYY-MM-DD hh24:mi:ss TZH:TZM TZD tzr') AS ts
FROM dual
CONNECT BY LEVEL <= 4;

+--------------------------------------------+
|TS                                          |
+--------------------------------------------+
|2005-10-30 01:00:00 +04:00 MSD Europe/Moscow|
|2005-10-30 02:00:00 +04:00 MSD Europe/Moscow|
|2005-10-30 02:00:00 +03:00 MSK Europe/Moscow|
|2005-10-30 03:00:00 +03:00 MSK Europe/Moscow|
+--------------------------------------------+

看看TIMESTAMP WITH TIME ZONE 数据类型

当时间从标准时间切换到夏令时时,要消除边界情况的歧义,请同时使用 TZR 格式元素和相应的 TZD 格式元素。TZD 格式元素是包含夏令时信息的时区区域的缩写。例如美国太平洋标准时间的 PST 和美国太平洋夏令时间的 PDT。以下规范确保返回夏令时值:

TIMESTAMP '1999-10-29 01:30:00 America/Los_Angeles PDT'

如果不添加 TZD 格式元素,并且 datetime 值不明确,则如果您将会话ERROR_ON_OVERLAP_TIME参数设置为,则 Oracle 数据库将返回错误TRUE。如果ERROR_ON_OVERLAP_TIME设置为FALSE(默认值),则 Oracle 数据库将不明确的日期时间解释为标准时间。

注意,时区+04:00+03:00等于Europe/Moscow时区Europe/Moscow考虑夏令时(大约 10 年前在俄罗斯仍在使用时),但+04:00/+03:00不考虑。

对不起,我从来没有使用过hibernate,所以我不知道这个框架是如何处理这些数据的。我也不熟悉Java。可能不支持夏令时信息。

我只能猜测,类java.sql.Date和方法setDate是指DATEOracle 中的数据类型。如前所述,更好地使用java.sql.TimestampsetTimestamp.

DATE数据类型不支持任何时区信息。如果您尝试向列中插入一个DATE值,TIMESTAMP WITH [LOCAL] TIME ZONE那么 Oracle 实际上会这样做

FROM_TZ(CAST(<your DATE value> AS TIMESTAMP), SESSIONTIMEZONE)
于 2020-12-14T17:52:18.090 回答
0

你的条件很特殊,我们尽量减轻一点。

Unix 时间 1130623200 是2005-10-29 22:00:00 UTC

  • 在莫斯科时间,这是2005-10-30 02:00:00 Europe/Moscow,但是这是模棱两可的。它可能是
  • 任何一个2005-10-30 02:00:00 +04:00 Europe/Moscow MSD
  • 或者2005-10-30 02:00:00 +03:00 Europe/Moscow MSK

Unix 时间 1130626800 是2005-10-29 23:00:00 UTC

  • 在莫斯科时间,这是2005-10-30 02:00:00 Europe/Moscow,但是这是模棱两可的。它可能是
  • 任何一个2005-10-30 02:00:00 +04:00 Europe/Moscow MSD
  • 或者2005-10-30 02:00:00 +03:00 Europe/Moscow MSK

根据 Oracle 文档TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow',标准时间的模棱两可,即2005-10-30 02:00:00 Europe/Moscow MSK +03:00(就像 2005 年一样!)

请注意,2005 年莫斯科标准时间是MSK => +03:00. 2011年,俄罗斯政府宣布未来全年实行夏令时,有效取代标准时间。即今天莫斯科标准时间是2011 年之前MSK => +04:00调用的。MSD

验证

SELECT 
    TO_CHAR(TIMESTAMP '2005-10-29 22:00:00 UTC' AT TIME ZONE 'Europe/Moscow', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS_1_UTC,
    TO_CHAR(TIMESTAMP '2005-10-29 23:00:00 UTC' AT TIME ZONE 'Europe/Moscow', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS_2_UTC,
    TO_CHAR(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS,
    TO_CHAR(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow MSK', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS_MSK,
    TO_CHAR(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow MSD', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS_MSD
FROM dual
TS_1_UTC TS_2_UTC TS TS_MSK TS_MSD
2005-10-30 02:00:00 +04:00 欧洲/莫斯科 MSD 2005-10-30 02:00:00 +03:00 欧洲/莫斯科 MSK 2005-10-30 02:00:00 +03:00 欧洲/莫斯科 MSK 2005-10-30 02:00:00 +03:00 欧洲/莫斯科 MSK 2005-10-30 02:00:00 +04:00 欧洲/莫斯科 MSD
ALTER SESSION SET TIME_ZONE = 'Europe/Moscow';

SELECT 
    TO_CHAR(CAST(TIMESTAMP '2005-10-29 22:00:00 UTC' AT TIME ZONE 'Europe/Moscow' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS_1_UTC,
    TO_CHAR(CAST(TIMESTAMP '2005-10-29 23:00:00 UTC' AT TIME ZONE 'Europe/Moscow' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS_2_UTC,
    TO_CHAR(CAST(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS,
    TO_CHAR(CAST(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow MSK' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS_MSK,
    TO_CHAR(CAST(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow MSD' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS_MSD
FROM dual
TS_1_UTC TS_2_UTC TS TS_MSK TS_MSD
2005-10-30 02:00:00 默沙东 2005-10-30 02:00:00 MSK 2005-10-30 02:00:00 MSK 2005-10-30 02:00:00 MSK 2005-10-30 02:00:00 默沙东

我认为 Oracle 的做法绝对正确,无论您是否拥有TIMESTAMP WITH TIME ZONETIMESTAMP WITH LOCAL TIME ZONE. 但是您必须仔细检查实际插入表格的内容以及如何显示它。

于 2020-12-25T11:47:11.763 回答