Postgres UTC 到 Java ZonedDateTime

son*_*rin 5 java spring-data-jpa postgresql-9.3

我原来的问题的后续问题

因为我知道使用的任何日期时间都是针对已知时区的,而不是用户可能从哪里提交他们的请求,所以我使用 LocalDateTime,转换为 UTC 并保持不变。然后,当检索到约会时,我将保存的时间转换为会议地点时区(存储在 db 中)。但是,我保存的值似乎实际上保存在我的本地时区中。

我在 Rest Controller 中收到一个日期时间值,例如:

startLocalDateTime: 2016-04-11T10:00
endLocalDateTime: 2016-04-11T10:30
Run Code Online (Sandbox Code Playgroud)

约会有两个 ZoneDateTime 字段:

@Column(name = "startDateTime", columnDefinition= "TIMESTAMP WITH TIME ZONE")
private ZonedDateTime startDateTime;
@Column(name = "endDateTime", columnDefinition= "TIMESTAMP WITH TIME ZONE")
private ZonedDateTime endDateTime;
Run Code Online (Sandbox Code Playgroud)

然后我将值更改为 UTC 并存储在我的实体中以存储到 Postgres:

appointment.setStartDateTime(startLocalDateTime.atZone(ZoneId.of( "UTC" )))
appointment.setEndDateTime(endLocalDateTime.atZone(ZoneId.of( "UTC" )))
Run Code Online (Sandbox Code Playgroud)

我将它存储在 Postgres(columnDefinition= "TIMESTAMP WITH TIME ZONE") 当我查看 pgadminIII 中的记录时,我看到:

startDateTime "2016-04-11 04:00:00-06"
endDateTime  "2016-04-11 04:30:00-06"
Run Code Online (Sandbox Code Playgroud)

所以这些似乎以UTC格式正确存储(如果我到目前为止做错了什么,请纠正我)。然后我从数据库中检索它们,它们返回为:

Appointment
startdatetime: 2016-04-11T04:00-06:00[America/Denver]
enddatetime: 2016-04-11T04:30-06:00[America/Denver]
Run Code Online (Sandbox Code Playgroud)

这些值作为 JSON 发送回:

{  
 "appointmentId":50,
 "startDateTime":"2016-04-11T04:00",
 "endDateTime":"2016-04-11T04:30"
}
Run Code Online (Sandbox Code Playgroud)

因此,即使我将它们保存为 UTC,当我检索它们时,它们位于 MST(我的本地)时区,而不是 UTC,并且我无法将它们转换回实际时间。

还在为坚持而苦苦挣扎。我曾尝试在我的实体上使用 java.sql.timestamp、java.sql.Date、java.util.Date 和 java.time.ZonedDateTime。我的 Postgres 仍然是“带时区的时间戳”。但是因为我使用的是 Spring-Data-JPA 并且需要使用相同的类型进行查询。如果我使用 Date - 应该是 sql.Date 还是 util.Date?

Bas*_*que 10

你说:

我使用 LocalDateTime,转换为 UTC 并坚持。

不。坚持LocalDateTime预订约会,约会不涉及UTC。

无论政治家如何重新定义在其管辖范围内的时区使用的偏移量,在预订应显示为特定时间的约会时,请存储带有时间的日期,但将时区分开。

世界各地的政客都表现出一种奇怪的嗜好,他们经常重新定义他们的区域的偏移,调整以匹配或区分他们的邻居,或者采用或放弃被称为夏令时 (DST)的愚蠢做法。这些变化几乎没有预先警告,甚至根本没有。所以现在看起来像未来一天的下午 2:30 可能会变成下午 1:30、下午 2:00、下午 3:30,或者谁知道政客们会想出什么,如果我们存储为一个时刻(日期、时间、区域/偏移量,所有这些)。

爪哇

要仅表示日期和时间,请使用LocalDateTime。这个类故意缺少任何时区或UTC偏移量的概念。这种缺乏意味着这个类并没有跟踪的时刻,是不是在时间轴上的一个点。所以我们一般不会在商业应用中使用这个类。预订未来的约会是我们确实想要这门课程的少数情况之一。

LocalDate ld = LocalDate.of( 2020 , Month.JANUARY , 23 ) ;    // 2020-01-23. 
LocalTime lt = LocalTime.of( 14 , 30 ) ;                      // 2:30 PM.
LocalDateTime ldt = LocalDateTime.of( ld , lt ) ;
Run Code Online (Sandbox Code Playgroud)

查看此代码在 IdeOne.com 上实时运行

ldt.toString(): 2020-01-23T14:30

使用 JDBC 4.2 及更高版本将其存储在您的数据库中。

myPreparedStatement.setObject( … , ldt ) ;
Run Code Online (Sandbox Code Playgroud)

从数据库中检索。

LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ); 
Run Code Online (Sandbox Code Playgroud)

我们需要跟踪预期的时区。这是魁北克牙医诊所的预约吗?也代表这个事实。请记住,时区是特定地区人民使用的 UTC 偏移量的过去、现在和未来(计划中的)更改的历史记录。

使用ZoneId该类来表示时区。

ZoneId z = ZoneId.of( "America/Montreal" ) ;
Run Code Online (Sandbox Code Playgroud)

将该区域名称作为文本保存到您的数据库中。通过调用获取其识别名称ZoneId::toString。千万不能使用ZoneId::getDisplayName,因为这是用于生成用于显示本地化的文本给用户,而不是该区域的正式标识。

myPreparedStatement.setString( … , z.toString() ) ;
Run Code Online (Sandbox Code Playgroud)

当您从数据库中检索时,重新构建ZoneId.

String zoneIdString = myResultSet.getString( … ) ;
ZoneId z = ZoneId.of( zoneIdString ) ;
Run Code Online (Sandbox Code Playgroud)

当您为这些约会生成日历时,如果您需要(暂时)确定时间线上的点,请将日期与时间和时区结合在一起。运用ZoneIdLocalDateTime产生ZonedDateTime。这ZonedDateTime 一个时刻,时间线上的一个点,不像LocalDateTime

 ZonedDateTime zdt = ldt.atZone( z ) ;
Run Code Online (Sandbox Code Playgroud)

查看此代码在 IdeOne.com 上实时运行

zdt.toString(): 2020-01-23T14:30-05:00[美国/蒙特利尔]

当然,我们存储那个ZonedDateTime。如上所述,如果那些忙碌的政客在现在和那时之间改变时区规则,那么现在似乎是 23 日下午 2:30 的这一刻将变成下午 1:30 或下午 3:30。我们只是ZonedDateTime暂时使用它。

数据库

我们LocalDateTime仅将 a 存储在类型为 的列中TIMESTAMP WITHOUT TIME ZONE。您错误地使用了WITH而不是WITHOUT,这是一个主要问题。对于WITHOUT类型,Postgres 存储输入中给出的日期和时间。即使输入中包含区域或偏移量,Postgres 也会忽略该区域或偏移量。

请注意,这TIMESTAMP WITH TIME ZONE在 Postgres 中正好相反。输入随附的时区或偏移量信息用于将日期和时间调整为 UTC。然后存储该 UTC 值。所以这种类型的列中的值都是UTC。不幸的是,许多工具具有在传递给您之前将默认时区动态应用于检索到的值的反特性。这会造成存储时区的错误错觉,而实际上该值是 UTC。检索作为OffsetDateTime遗嘱总是告诉你真相。我这么说OffsetDateTime是因为,奇怪的是,JDBC 4.2 规范需要支持该类,但不需要支持更常用的Instant&ZonedDateTime类。您的 JDBC 驱动程序可以选择支持其他两个类。但本段的所有内容都不在这里也不在那里,因为我们不会WITH在未来的约会中使用专栏。

如上所述,对于未来的约会应存储在三 (3) 个单独的列中:

  • TIMESTAMP WITHOUT TIME ZONE 日期和时间。
  • TEXT (或类似类型)用于我们要查看该约会的预期时区的识别名称。
  • TEXT(或类似)在任命期间,采用标准 ISO 8601 格式。(见下文)

问题中的要点

你说:

我在 Rest Controller 中收到一个日期时间值,例如:

startLocalDateTime: 2016-04-11T10:00

所以解析为LocalDateTime. 此类符合 ISO 8601 的字符串可以由java.time类直接解析。

LocalDateTime ldt = LocalDateTime.parse( "2016-04-11T10:00" ) ;
Run Code Online (Sandbox Code Playgroud)

你说:

endLocalDateTime: 2016-04-11T10:30

不。不要存储结束时间。如果当时发生时区异常,您现在预期的结束时间可能会有所不同。例如,如果您的政客采用夏令时 (DST),那么在这次任命期间,发生了“提前”1 小时的夏令时变化怎么办?那么您的半小时会议应该从上午 10:00 开始到上午 11:30,而您记录的结束时间将错误地显示为“上午 10:30”。

通常,处理约会的最佳方式是记录约会的持续时间、时间跨度,而不是在时钟上。如上所述,约会由三部分独立的数据组成:(1) 开始日期和时间,(2) 预期时区,以及 (3) 约会的持续时间。结尾应根据需要动态计算,使用新的当前时区数据,以供临时使用。

ISO 8601标准包括一个文本格式记录这些持续时间:PnYnMnDTnHnMnS其中,P标记开始,T分离任何年-月-日从任何小时,分钟,秒。该java.timePeriodDuration可以分析这样的字符串。

Duration d = Duration.ofHours( 1 ) ;
String output = d.toString() ;
Run Code Online (Sandbox Code Playgroud)

查看此代码在 IdeOne.com 上实时运行。

输出:PT1H

您可以将此持续时间动态应用于LocalDateTime.

LocalDateTime ending = ldt.plus( d ) ;
Run Code Online (Sandbox Code Playgroud)

同上ZonedDateTime

ZonedDateTime ending = zdt.plus( d ) ;
Run Code Online (Sandbox Code Playgroud)

你说:

约会有两个 ZoneDateTime 字段:

不。使它成为一个LocalDateTime字段,而不是ZonedDateTime.

你说:

@Column(name = "startDateTime", columnDefinition="TIMESTAMP WITH TIME ZONE")

不。将其设为“TIMESTAMP WITHOUT TIME ZONE”类型的列,以在没有区域/偏移量上下文的情况下跟踪日期和时间。

你说:

然后我将值更改为 UTC 并存储在我的实体中以存储到 Postgres:

不。预订约会是我们不想要 UTC 的少数情况之一。在跟踪某个时刻时,我们通常确实希望使用 UTC。但未来的约会不是时刻。在我们将时区应用于日期时间之前,我们不知道时刻,我们不能提前这样做,因为我们不能信任政治家。

你说:

还在为坚持而苦苦挣扎。

不能帮你。我不使用 Spring 或 JPA,因为它们解决了我没有的问题。我使用直接的JDBC。

你说:

所以这些似乎以UTC格式正确存储(如果我到目前为止做错了什么,请纠正我)。

不,不要使用 UTC,不要以 UTC 存储,而不是用于约会。

这样做的时刻使用UTC虽然。例如,您的日志都应该以 UTC 时间报告事件的每一刻。程序员和系统管理员在工作时最好学会用 UTC 思考。将桌面上的第二个时钟设置为 UTC。

你说:

当我检索它们时,它们位于 MST(我的本地)时区,而不是 UTC,我无法将它们转换回实际时间。

在我们的预约中,我们没有使用 UTC,也没有使用任何其他时区或与 UTC 的偏移量。所以你的问题蒸发了。

提示:正如我上面所说,您的工具可能会在时区方面对您撒谎。如果需要,将工具和您的会话设置为 UTC,以消除此反功能。

你说:

我曾尝试在我的实体上使用 java.sql.timestamp、java.sql.Date、java.util.Date 和 java.time.ZonedDateTime。

  • 永远不要使用java.sql.Timestamp. 替换为OffsetDateTime(和Instant)。
  • 永远不要使用java.sql.Date. 替换为LocalDate
  • 永远不要使用java.util.Date. 替换为Instant

仅使用现代java.time类,这些类多年前取代了与最早版本的 Java 捆绑在一起的那些糟糕的日期时间类。随着JSR 310的采用,那些糟糕的类成为了遗产。

Java(旧版和现代版)和标准 SQL 中的日期时间类型表。


Ira*_*eau 1

jdbc 驱动程序对您当前所在的时区有一些了解。通常,我过去通过让数据库为我执行时区转换来解决这个问题,这是“没有时区的时间戳 AT TIME ZONE zone”或“时间戳”的衍生词时区为“UTC”时区”。postgres jdbc 驱动程序的内部是确定 JVM 所在的时区并在保存中使用它。