【问题标题】:After retrieving Timestamp with Timezone from postgres date increases by 1. Why?从 postgres 中检索带时区的时间戳后,日期增加 1。为什么?
【发布时间】:2017-04-28 05:07:08
【问题描述】:

我从 postgres 获取带有时区值的时间戳,在 java 中我将结果存储在 Timestamp 变量中。但是日期时间会以某种方式发生变化。我没有得到原始值。

表格

CREATE TABLE log_fail
(
  user_name character varying(99) NOT NULL,
  date_time timestamp with time zone,
  CONSTRAINT pk_log_failed_login PRIMARY KEY (user_name)
)

表格数据

用户名 = 超人

date_time = 2016-12-12 10:06:39.582-08

sql

String uname = "超人";

String sql1 = "select date_time from log_failed_login where user_name = '" + uname + "'";

  rs1 = conn.createStatement().executeQuery(sql1);

  if (rs1.next()) 
        {
  Timestamp attempt_datetime = rs1.getTimestamp("date_time");
        }

我得到的结果

attempt_datetime = 2016-12-12 23:36:39.582

【问题讨论】:

  • ` 2016-12-12 23:36:39.582` 接近午夜 => 时区
  • 您能说出数据库中存储的时区以及运行应用程序代码的时区吗?存储在数据库中的时区信息将帮助您以正确的格式进行日期时间转换。我假设它被转换为应用程序时区。
  • 恐怕数据库中没有存储时区 - 它始终是UTC
  • 我已经给出了我数据库中的所有内容。数据库有 >> date_time = 2016-12-12 10:06:39.582-08

标签: java postgresql date timestamp


【解决方案1】:

三个偏移量

您没有向我们展示您的所有代码,因此我们无法肯定地回答。但是经过一些推论,我有一个理论,一个三偏移理论。

从比 UTC 晚 8 小时的值开始 → 2016-12-12 10:06:39.582-08

调整为 UTC → 2016-12-12 18:06:39.582Z

调整为+05:30的偏移量→2016-12-12 23:36:39.582+05:30

尴尬地报告了无偏移量→2016-12-12 23:36:39.582

感谢the Answer of Laurenz Albe解决核心问题;我只是在这里添加了一个冗长的叙述。

几个术语:

  • offset-from-UTC 是从 UTC 开始的小时数、分钟数和秒数,例如 +05:00
  • 时区是一个偏移量加上一组用于处理异常情况的规则,例如夏令时 (DST)。正确命名为continent/region,例如America/Montreal

您的输入 2016-12-12 10:06:39.582-08 表示比 UTC 晚 8 小时,在北美西海岸的大部分地区使用。

您将该值提交给 Postgres 进行存储。您没有准确地说,但我假设表中的列是TIMESTAMP WITH TIME ZONE 类型。

在 Postgres 中,这种类型的意思是:“对于任何附带的偏移量/区域信息,将传入的值调整为 UTC”。 Postgres 然后丢弃随附的偏移量/区域信息。我再说一遍,Postgres 存储或记住偏移量/区域,而不是任何日期时间类型。 WITH 类型和 TIMESTAMP WITHOUT TIME ZONE 之间的区别在于,在 WITHOUT 类型中,任何随附的偏移量/区域信息都将被完全忽略,不进行任何调整。

因此,当您向 Postgres 提交 2016-12-12 10:06:39.582-08 时,该值会自动调整为 2016-12-12 18:06:39.582ZZ 这里是 Zulu 的缩写,意思是 UTC。此外,Postgres 按字面意思存储这些字符串,而是使用其自己的内部二进制存储格式。这些字符串供我们人类在本次讨论中阅读。

无论如何,回到对 UTC 的调整。请注意时间是如何从 10 小时更改为 18 小时的,因为从 UTC 晚 8 小时调整为 UTC 意味着增加了 8 小时。

所以,很好,存储这个值的工作已经完成。我们记录了完全相同的时刻,2016-12-12 10:06:39.582-082016-12-12 18:06:39.582Z 都是相同的时刻,但通过两个不同的 wall-clock time 值的镜头可以看到。到目前为止,一切都是正确和明智的。

后来,您从 Postgres 数据库中检索了 2016-12-12 18:06:39.582Z 值。显然,您通过 JDBC 将数据提取到 java.sql.Timestamp 对象中。问题就在于此。此类是与 Java 的最早版本捆绑在一起的旧日期时间类之一。虽然作为行业领先的日期时间处理尝试值得称赞,但这些课程已被证明是令人困惑、麻烦和有缺陷的。尽可能避免使用它们。它们现在是 legacy,被 java.time 类所取代。

在这些遗留日期时间类的许多问题中,它们的toString 方法的实现。你的toString 方法过于急于取悦隐式应用 JVM 的当前时区。这些对许多 Java 程序员造成了无穷无尽的困惑。添加这个问题作为这种混淆的另一个例子。

首先,让我们演示一下时区的这种隐式应用。首先以Instant 的形式获取UTC 的当前时刻。

Instant instant = Instant.now ();

查看我们的 JVM 当前的默认时区及其偏移量。

ZoneId z = ZoneId.systemDefault ();  // Get current default time zone of this JVM.
String offset = z.getRules ().getOffset ( instant ).toString ();  // Extract the offset-from-UTC from our default time zone.

最后,创建一个java.sql.Timestamp 以查看其toString 方法的行为。

java.sql.Timestamp ts = new java.sql.Timestamp ( instant.toEpochMilli () );

转储到控制台。

System.out.println ( "instant.toString(): " + instant );
System.out.println ( "z.toString(): " + z );
System.out.println ( "offset.toString(): " + offset );
System.out.println ( "ts.toString(): " + ts );

instant.toString(): 2016-12-13T23:06:22.635Z
z.toString(): America/Los_Angeles
offset.toString(): -08:00
ts.toString(): 2016-12-13 15:06:22.635

从此输出中我们可以看到Timestamp 确实将我自己的当前默认值-08:00 偏移量应用于UTC 的内部值。更糟糕的是,这个方法的输出忽略了它的偏移量的任何指示,让我们假设它是 UTC,而实际上不是。更令人沮丧的是:this ‘feature’ is entirely undocumented

现在我们已经看到了这个反特征的行为,我们可以回到问题中的示例数据。显然,结果数据是2016-12-12 23:36:39.582,没有我们上面提到的任何偏移量的迹象。但是看看一天中的时间,23:36:39.582 与我们可能预期的 UTC,18:06:39.582。意外的值比我们预期的 UTC 值早五个半小时,即+05:30+05:30 的偏移量恰好是 used in India 和斯里兰卡,在诸如 Asia/Kolkata 之类的区域中。所以我们可以推断出这个问题的作者正在JVM上运行她的代码,当前默认时区类似于Asia/ColomboAsia/Kolkata,偏移量为+05:30

解决方案

避免使用旧的日期时间类。

改用 java.time 类。 java.time 类都有简单的toString 方法,可以合理地使用标准的ISO 8601 文本格式,没有任何意外的奖励行为。

您的JDBC 4.2 兼容driver 可以通过调用PreparedStatement::setObjectResultSet::getObject 直接寻址java.time 类型。

myPreparedStatement.setObject( … , instant ) ;

……和……

Instant instant = myResultSet.getObject( … ) ;

如果不是,则回退到使用 java.sql 类型,但要尽可能简短。使用添加到旧类的新转换方法。

myPreparedStatement.setTimestamp( … , java.sql.Timestamp.from( instant ) ) ;

……和……

Instant instant = myResultSet.getTimestamp( … ).toInstant() ;

关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

从哪里获取 java.time 类?

ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

【讨论】:

  • 很好的答案,正确的解释,上帝我讨厌时区。
【解决方案2】:

问题是运行Java代码的机器上的时区是Asia/Calcutta,而时间戳转换为字符串时的时区。

您可以像这样在 Java 中更改您的时区:

java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("Europe/Vienna"));

【讨论】:

  • 设置 JVM 当前的默认时区是一个激进的解决方案。这样做会立即影响在该 JVM 中运行的所有应用程序的所有线程中的所有代码。反过来,任何其他代码在任何时候都可以在您背后再次更改区域,从而使这种方法不可靠。更好的解决方案是在调用 Java 方法时始终明确指定所需/预期的时区,而不是隐式依赖默认时区。
  • 这个答案似乎是对问题的正确诊断。我上面的评论根本不同意这里提出的解决方案。
【解决方案3】:

https://www.postgresql.org/docs/current/static/datatype-datetime.html

对于带时区的时间戳,内部存储的值始终在 UTC(通用协调时间,传统上称为格林威治标准时间) 时间,格林威治标准时间)。指定了明确时区的输入值是 使用该时区的适当偏移量转换为 UTC。如果 输入字符串中没有说明时区,则假定为 在系统的 TimeZone 参数指示的时区中,并且是 使用时区的偏移量转换为 UTC。

当一个带有时区值的时间戳被输出时,它总是 从 UTC 转换为当前时区,并显示为 该地区的当地时间

检查您客户的时区:

select setting from pg_settings where name = 'TimeZone';

这会给你一个想法,为什么不同客户的时间不同

【讨论】:

    猜你喜欢
    • 2012-08-08
    • 1970-01-01
    • 2011-12-27
    • 2019-11-08
    • 1970-01-01
    • 2011-05-26
    • 1970-01-01
    • 1970-01-01
    • 2021-01-05
    相关资源
    最近更新 更多