将约会存储在 SQL 数据库中,例如用于 java.time 框架的 Postgres

Storing appointments in a SQL database such as Postgres for use with java.time framework

假设我们在 Milan Italy 有一个约会,时间是 01/23/2021 21:00“Europe/Rome”。此约会以类似于 SQL 标准类型 TIMESTAMP WITH TIME ZONE.

的类型的列保存到 UTC 数据库中

现在居住在 New York US 的用户需要了解此约会何时进行。我们可以向用户显示转换为“America/New_York”时区的日期时间,或者改为“Europe/Rome”时区。一旦用户从纽约飞往米兰,他会发现这两个信息都很有用。

重点是存储转换为相同 TZ 参考 (UTC) 的所有内容,并根据您使用与现代 Java 捆绑的 java.time 框架的目标来操作日期时间.

有道理还是有道理wrong/missing?

Makes sense or there is something wrong/missing?

这取决于约会的种类。

有两种约会:

  • 时刻,时间轴上的特定点,忽略时区规则的任何更改。
    示例:火箭发射。
  • 日期和 time-of-day 应根据时区规则的更改进行调整。
    示例:Medical/Dental 访问。

时刻

例如,如果我们要预订火箭发射,我们不关心日期和 time-of-day。我们只关心 (a) 天高气爽,(b) 期待风调雨顺的时刻。

如果在此期间政客们改变了我们发射场或我们办公室使用的时区规则,这对我们的发射任命没有影响。如果管理我们发射场的政客与邻国采用Daylight Saving Time (DST), the moment of our launch remains the same. If the politicians governing our offices decide to change the clock a half-hour earlier because of diplomatic relations,我们发射的时刻保持不变。

对于这样的约会,是的,你的做法是正确的。您可以使用 TIMESTAMP WITH TIME ZONE 类型的列将约会记录在 UTC 中。检索后,调整到用户喜欢的任何时区。

Postgres use any time zone info accompanying an input to adjust into UTC, and then disposes of that time zone info. When you retrieve the value from Postgres, it will always represent a date with time-of-day as seen in UTC. Beware, some tooling or middleware may have the anti-feature of applying some default time zone between retrieval from database and delivery to you the programmer. But be clear: Postgres always saves and retrieves values of type TIMESTAMP WITH TIME ZONE in UTC, always UTC, and offset-from-UTC等数据库hours-minutes-seconds.

这里是一些示例 Java 代码。

LocalDate launchDateAsSeenInRome = LocalDate.of( 2021 , 1 , 23 ) ;
LocalTime launchTimeAsSeenInRome = LocalTime.of( 21 , 0 ) ;
ZoneId zoneEuropeRome = ZoneId.of( "Europe/Rome" ) ;
// Assemble those three parts to determine a moment.
ZonedDateTime launchMomentAsSeenInRome = ZonedDateTime.of( launchDateAsSeenInRome , launchTimeAsSeenInRome , zoneEuropeRome ) ;

launchMomentAsSeenInRome.toString(): 2021-01-23T21:00+01:00[Europe/Rome]

要以 UTC 格式查看同一时刻,请转换为 InstantInstant 对象始终表示 UTC 中的时刻。

Instant launchInstant = launchMomentAsSeenInRome.toInstant() ;  // Adjust from Rome time zone to UTC.

launchInstant.toString(): 2021-01-23T20:00:00Z

上述字符串示例末尾的Z是UTC的标准表示法,发音为“Zulu”。

不幸的是,JDBC 4.2 团队忽略了对任何一个 Instant or ZonedDateTime types. So your JDBC driver may or may not be able to read/write such objects to your database. If not, simply convert to OffsetDateTime. All three types represent a moment, a specific point on the timeline. But OffsetDateTime has support required by JDBC 4.2 的支持,原因让我难以理解。

OffsetDateTime odtLaunchAsSeenInRome = launchMomentAsSeenInRome.toOffsetDateTime() ;

正在写入数据库。

myPreparedStatement.setObject( … , odtLaunchAsSeenInRome ) ;

从数据库中检索。

OffsetDateTime launchMoment = myResultSet.getObject( … , OffsetDateTime.class ) ;

调整为您的用户所需的纽约时区。

ZoneId zoneAmericaNewYork = ZoneId.of( "America/New_York" ) ;
ZonedDateTime launchAsSeenInNewYork = launchMoment.atZoneSameInstant( zoneAmericaNewYork ) ;

launchAsSeenInNewYork.toString(): 2021-01-23T15:00-05:00[America/New_York]

以上都可以看到code run live at IdeOne.com.

顺便说一下,追溯往事也算一个瞬间。病人什么时候真正到达预约,客户什么时候支付发票,新员工什么时候签署他们的文件,服务器什么时候崩溃......所有这些都在 UTC 中被跟踪为一个时刻。如上所述,通常这将是一个 Instant,尽管 ZonedDateTimeOffsetDateTime 也代表一个时刻。对于数据库使用 TIMESTAMP WITH TIME ZONE(不是 WITHOUT)。

Time-of-day

我预计大多数 business-oriented 应用程序都专注于其他类型的约会,我们的目标是 time-of-day 而不是特定时刻的约会。

如果用户与他们的医疗保健提供者预约查看测试结果,他们会在该日期的特定 time-of-day 进行。如果与此同时政客们改变了他们的时区规则,将时钟提前或推迟一个小时或 half-hour 或任何其他时间量,该医疗预约的日期和 time-of-day 保持不变.实际上,政客们改变时区后,原来任命的时间线点也会发生变化,转移到时间线上的earlier/later点。

对于此类约会,我们不会 存储日期和 time-of-day,如 UTC 所示。我们使用数据库列类型TIMESTAMP WITH TIME ZONE

对于此类约会,我们将日期存储为 time-of-day,而不考虑时区。我们使用 TIMESTAMP WITHOUT TIME ZONE 类型的数据库列(注意 WITHOUT 而不是 WITH)。 Java中的匹配类型是LocalDateTime.

LocalDate medicalApptDate = LocalDate.of( 2021 , 1 , 23 ) ;
LocalTime medicalApptTime = LocalTime.of( 21 , 0 ) ;
LocalDateTime medicalApptDateTime = LocalDateTime.of( medicalApptDate , medicalApptTime ) ;

将其写入数据库。

myPreparedStatement.setObject( … , medicalApptDateTime ) ;

明确这一点:一个LocalDateTime对象不是代表一个时刻,是不是上的一个特定点时间线。 LocalDateTime 对象表示时间轴(全球时区范围)中大约 26-27 小时的 可能 时刻范围。要赋予 LocalDateTime 真正的意义,我们必须关联预期的时区。

对于预期的时区,使用第二列来存储时区标识符。例如,字符串 Europe/RomeAmerica/New_York。参见 list of zone names

ZoneId zoneEuropeRome = ZoneId.of( "Europe/Rome" ) ;

将其作为文本写入数据库。

myPreparedStatement.setString( … , zoneEuropeRome ) ;

检索。检索区域名称作为文本,并实例化一个 ZoneId 对象。

LocalDateTime medicalApptDateTime = myResultSet.getObject( … , LocalDateTime.class ) ;
ZoneId medicalApptZone = ZoneId.of( myResultSet.getString( … ) ) ;

将这两个部分放在一起以确定表示为 ZonedDateTime 对象的时刻。当您需要安排日历时动态执行此操作。但是不要存储这个时刻。如果未来政治家重新定义时区,那么必须计算不同的时刻。

ZonedDateTime medicalApptAsSeenInCareProviderZone = ZonedDateTime.of( medicalApptDateTime , medicalApptZone ) ;

用户正在前往美国纽约。他们需要根据纽约临时住所墙上的时钟知道何时致电意大利米兰的医疗保健提供者。因此,从一个时区调整到另一个时区。同样的时刻,不同的 wall-clock 时间。

ZoneId zoneAmericaNewYork = ZoneId.of( "America/New_York" ) ;
ZonedDateTime medicalApptAsSeenInNewYork = medicalApptAsSeenInCareProviderZone.withZoneSameInstant( zoneAmericaNewYork ) ;

tzdata

请注意,如果您所需的时区规则可能发生变化,您必须更新计算机上的时区定义副本。

Java 包含它自己的 tzdata 副本,Postgres 也是如此数据库引擎。还有你的主机操作系统。此处显示的这个特定代码只需要 Java 为 up-to-date。如果你用Postgres做时区调整,它的tzdata也必须是up-to-date。对于日志记录等,您的主机 OS 应该保持 up-to-date。对于用户正确的 clock-watching,他们的客户端机器的 OS 也必须是 up-to-date.

注意:世界各地的政客都表现出以惊人的频率改变时区的倾向,而且往往没有预先警告。


关于java.time

java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

Joda-Time project, now in maintenance mode, advises migration to the java.time 类.

您可以直接与您的数据库交换 java.time 对象。使用 JDBC driver compliant with JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* 类。 Hibernate 5 和 JPA 2.2 支持 java.time.

在哪里获取java.time类?

一个非常简单的解决方案是将转换留给 PostgreSQL。如果您为每个会话正确设置 timezone 参数并使用 timestamp with time zone PostgreSQL 将在纽约时间自动向纽约用户显示时间戳。