【问题标题】:Dealing with DateTime and Timezones with Java / Postgres使用 Java / Postgres 处理日期时间和时区
【发布时间】:2019-01-08 03:36:35
【问题描述】:

我正在创建一个应用程序,让用户创建一个带有日期的事件。例如 - 生日,25.09.2018。 (只要求用户提供日期)。我想将这些事件对象存储在我的 Postgres DB 中。

在该日期结束时(这意味着 2018 年 9 月 25 日的 23:59:59)我希望我的应用程序在事件 expired = true 上设置一个标志。

我最初的想法是每整小时运行一次调度程序,检查事件记录,如果当前服务器时间大于存储的事件时间,则设置标志。 因此,我的第一个问题是: 调度程序的想法是最好的方法吗?或者它的性能可能有点差,实际上有更好的方法来做到这一点?我想每小时运行一次的原因是因为时区不同。某处总是有 23:59:59。

其他问题更侧重于存储和检查日期信息本身。我如何处理时区?假设来自美国的用户在给定日期添加了生日活动,而我的服务器位于欧洲。 如何将其存储在 PostgresDB 中以及如何在调度程序中执行检查?我需要以某种方式将时区存储在数据库中,然后在调度程序中检查在给定的时区中是否是给定服务器时间的 23:59:59 的日期。

请帮忙,我真的很困惑。一个例子将是惊人的!谢谢!

【问题讨论】:

  • 提示:在日期时间处理中,定义时间跨度的最佳实践通常是半开放方法,其中开始是 inclusive 而结束是独家。示例:午休时间从中午到下午 1 点,学生在在下午 1 点钟声敲响之前回到座位。一周从星期一开始,一直持续到但不包括下一个星期一。因此,与其努力确定一天的最后一刻,不如将重点放在下一天的第一刻作为你的结局。在 Java 中,请参阅 LocalDate::atStartOfDay( ZoneId )。

标签: java postgresql date timezone scheduled-tasks


【解决方案1】:

Answer by Albe 在我看来是正确的。以下是我的一些进一步的想法。

我希望我的应用程序为 event expired = true 设置一个标志。

正如 Albe 所提到的,没有必要记录这个过期的事实。只需按日期查询即可查找日期之前(过期)或日期之后(未过期)的记录。

如果性能是一个问题,请进行测试以验证。如果有问题,请考虑为日期列编制索引。

如何处理时区?

这就是诀窍。我将在这里简要介绍一下,因为这已经在这里和姊妹网站https://dba.stackexchange.com 上处理了很多次。

首先,弄清楚您的业务需求是否需要日期或时刻的粒度。正如你直觉的那样,日期是不精确的。对于任何给定的时刻,日期在全球范围内因时区而异。法国巴黎午夜过后几分钟是新的一天,而魁北克蒙特利尔仍是“昨天”。

如果您关心时刻,请使用类似于 SQL 标准 TIMESTAMP WITH TIME ZONE 的类型的列。了解 SQL 标准几乎不涉及日期时间处理的主题。因此,不同数据库系统中的行为差异很大。在 Postgres 中,该列中的值始终为 UTC。与输入一起提供的任何时区或与 UTC 的偏移信息都用于调整为 UTC,然后被丢弃。因此,如果您关心原始区域/偏移量,则必须手动将其存储在附加列中。

检索时,该列的值始终采用 UTC。但是,您使用的访问工具可能会注入其关于如何呈现该值的意见。一些工具会动态应用时区。我发现这种行为是一个不幸的设计选择。尽管出于善意,但这种行为会造成该区域是存储数据的一部分的错觉。

在 Java 中,只使用现代的 java.time 类,而不是与最早的 Java 版本捆绑在一起的非常麻烦和有缺陷的旧日期时间类。

从 JDBC 4.2 开始,您可以使用 PreparedStatement::setObject 和 ResultSet::getObject 直接与数据库交换 java.time 对象。

我的服务器位于欧洲

服务器的位置以及服务器操作系统和 JVM 的当前默认时区应该与您的编程和数据库工作无关。永远不要依赖这些默认设置,因为它们超出您的控制范围,并且可以在运行时随时更改。

相反,请始终明确指定您想要/预期的时区。通常,您的大部分工作都应该使用 UTC。仅在业务规则需要或向用户展示时才调整到时区。

LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ;
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = ld.atStartOfDay( z ) ;

通过提取Instant 调整回UTC。 Instant 是您应该与数据库交换的内容。

Instant instant = zdt.toInstant() ;

调整回一个区域。

ZonedDateTime zdtAuckland = instant.atZone( ZoneId.of( "Pacific/Auckland" ) ;  // Same moment, same point on the timeline, different wall-clock time.

要查询时刻,请搜索 Stack Overflow 以了解半开放方法,其中定义了一个时间跨度,其中开始包含,而结束不包含时间>。这意味着您不使用 SQL 运算符 BETWEEN。例如,请参阅this。

提示:使用具有site:stackoverflow.com 标准的搜索引擎搜索这些类名称和概念。不幸的是,Stack Overflow 内置的搜索功能偏向于问题,而忽略了答案。例如:https://duckduckgo.com/?q=site%3Astackoverflow.com+%2B%22Half-Open%22+%2Bjava&t=osx&ia=web


关于java.time

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

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

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

您可以直接与您的数据库交换 java.time 对象。使用符合JDBC 4.2 或更高版本的JDBC driver。不需要字符串,不需要java.sql.* 类。

从哪里获得 java.time 类?

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

【讨论】:

    【解决方案2】:

    如果您希望能够按时区使数据过期,则必须存储用户的时区。我的建议是不要在行过期时更改行,而是在查询时计算它是否过期:

    SELECT e.event_date::timestamp
           AT TIME ZONE u.timezone
           <= current_timestamp - INTERVAL '1 day' AS expired
    FROM event e JOIN user u ON ...
    

    【讨论】:

      猜你喜欢
      • 2015-11-13
      • 2013-03-05
      • 2013-06-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-20
      相关资源
      最近更新 更多