【问题标题】:Handling time zone in web application在 Web 应用程序中处理时区
【发布时间】:2015-10-26 10:51:22
【问题描述】:

在我们的网络应用程序中,我们需要显示并输入 不同时区不同国家的日期时间信息。目前,我们正在为每个国家/地区维护单独的 Web 服务器和单独的数据库(oracle 11g)。

我们计划将所有内容合并到一个具有单一数据库(oracle 11g)的门户中。 此门户应在用户本地时区捕获/显示日期和时间。

到目前为止,我已经搜索过这个,我得到了以下建议。

1) 将 Web 服务器和数据库服务器的时区设置为 UTC,并在获取数据(数据和时间)时转换为用户本地时区。

如果您建议这种方法,请澄清以下具体内容 问题。

  • 我们大部分时间都是单独捕获日期,是否需要 始终捕获日期和时间以及时区?

  • 同时存储我们需要转换用户本地时间的日期和时间 在 javascript/java/oracle 中区域到 UTC?

  • 在获取我们需要将 UTC 转换为用户的日期和时间时
    本地时区查询本身/java/java 脚本?

  • 我们在很多地方都有根据日期列显示的报告,例如
    今天/当前月份/日期范围。我们如何处理这个(输入 - 用户 本地时区 - UTC 数据库)?

  • 我们必须为日期字段使用哪种数据类型 (日期/时间戳/带有时区的时间戳/带有本地时间的时间戳 区)?

2) 在用户本地时区和 UTC 中捕获日期和时间。存储为单独的列,用户本地时区将用于显示目的,UTC 将用于业务逻辑。

如果您建议这种方法,请澄清以下具体内容 问题。

  • 存储用户本地时区和UTC是常见的做法吗?

  • 在获取要显示的报告时,我必须检查哪一列的条件 基于日期列,例如今天/当前月份/日期范围?

  • 我们必须为日期列使用哪种数据类型
    (日期/时间戳/带有时区的时间戳/带有本地时间的时间戳
    区)?

提前致谢

【问题讨论】:

  • 如果插入数据的用户和查询数据的用户在不同的时区,你会看到什么?
  • @Gopi 你能重写倒数第二个项目符号更清楚吗?

标签: java oracle date datetime timezone


【解决方案1】:

阅读问题Daylight saving time and time zone best practices。你的基本上是重复的。

UTC 服务器

是的,通常服务器应将其操作系统设置为UTC 作为时区,或者如果未提供,则使用GMTReykjavík Iceland time zone。您的 Java 实现可能会将此设置作为其自己的当前默认时区。

指定时区

但不要依赖于设置为 UTC 的时区。系统管理员可以更改它。 JVM 内任何应用程序的任何线程中的任何 Java 代码都可以通过调用 TimeZone.setDefault 来更改 JVM 当前的默认时区在运行时。因此,请养成始终通过在 Java 代码中传递可选参数来指定所需/预期时区的习惯。

我认为任何日期时间框架都会使时区成为可选的设计缺陷。可选会造成无穷无尽的混乱,因为程序员和其他人一样,除非有提示,否则会无意识地根据自己的个人时区进行思考。因此,在日期时间的工作中,常常没有注意到这个问题。加上 JVM 默认值不同的问题。顺便说一句,Locale 同上,同样的问题,应该始终明确指定。

UTC

您的业务逻辑、数据存储和数据交换几乎都应该在UTC 中完成。几乎每个数据库都具有将任何输入调整为 UTC 并以 UTC 存储的功能。

向用户显示日期时间时,请调整到预期的时区。序列化日期时间值时,使用ISO 8601 字符串格式。请参阅 VickyArora 为 Oracle 撰写的 the Answer(我是 Postgres 人)。请务必仔细阅读文档,并通过实验来充分理解数据库的行为。 SQL 规范在这方面没有详细说明,而且行为差异很大。

java.sql

请记住,当使用 Java 和 JDBC 时,您将使用 java.sql.Timestamp 和相关的数据类型。它们总是自动使用 UTC。将来期望看到更新 JDBC 驱动程序以直接使用 Java 8 及更高版本中内置的 java.time 框架中定义的新数据类型。

java.time

旧类已被java.time 淘汰。 学习使用 java.time,同时避免使用旧的 java.util.Date/.Calendar,让您的编程生活更加愉快。

在您的JDBC driver 更新之前,您可以使用java.time 中内置的转换便利方法。请看下面的示例,其中Instant 是 UTC 中的时刻,ZonedDateTime 是调整为 time zone 的 Instant。

Instant instant = myJavaSqlTimestamp.toInstant();
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

去另一个方向。

java.sql.Timestamp myJavaSqlTimestamp = java.sql.Timestamp.from( zdt.toInstant() );

如果您需要原始时区,请存储它

如果您的业务需求认为原始输入数据的时区很重要,需要记住,则将其显式存储为数据库表中的单独列。您可以使用offset-from-UTC,但这并不能提供完整的信息。时区是一个偏移量加上一组过去、现在和将来异常处理的规则,例如Daylight Saving Time。所以proper time zone name 是最合适的,例如America/Montreal

仅日期不明确

您说您收集了许多仅日期值,没有时间和时区。 java.time 中的类是LocalDate。与LocalTimeLocalDateTime 一样,“本地...”部分表示没有特定的地点,因此没有时区,因此也不是时间线上的一个点——没有真正的意义。

请记住,仅日期值在定义上是不明确的。在任何特定时刻,世界各地的日期都会有所不同。例如,Paris France 的午夜刚过是新的一天,但Montréal Québec 的日期仍然是“昨天”。

通常在商业中,某些时区是隐含的,甚至是无意识的直觉。从长远来看,对数据点的无意识直觉往往效果不佳,尤其是在软件中。最好明确说明预期的时区。您可以将预期区域存储在日期旁边,例如数据库表中的另一列,或者您可以在您的编程代码中进行注释。我相信存储日期时间值会更好、更安全。那么我们如何将仅日期转换为日期时间呢?

新的一天通常是午夜过后的那一刻,即一天的第一刻。您可能认为这意味着一天中的时间00:00:00.0,但并非总是如此。 Daylight Saving Time (DST) 和可能的其他异常可能会将第一时刻推到不同的 wall-clock time。让 java.time 确定第一次通过 LocalDate 类及其 atStartOfDay 方法的正确时间。

ZoneId zoneId = ZoneId.of( "America/Montreal" );
LocalDate today = LocalDate.now( zoneId );
ZonedDateTime todayStart = today.atStartOfDay( zoneId );

在某些业务环境中,新的一天可能被定义(或假定)为工作时间。例如,假设New York 的出版商在他们说“书稿将于 1 月 2 日到期”时表示他们当地时间上午 9 点。让我们在那个时区获取那个日期的那个时间。

ZoneId zoneId = ZoneId.of( "America/New_York" );
ZonedDateTime zdt = ZonedDateTime.of( 2016 , 1 , 2 , 9 , 0 , 0 , 0 , zoneId );

这对在New Zealand 工作的作者意味着什么?通过调用withZoneSameInstant调整到她的particular time zone以向她展示。

ZoneId zoneId_Pacific_Auckland = ZoneId.of( "Pacific/Auckland" );
ZonedDateTime zdt_Pacific_Auckland = zdt.withZoneSameInstant( zoneId_Pacific_Auckland );

数据库

对于数据库存储,我们转换为Instant(UTC 时间线上的时刻)并作为java.sql.Timestamp 传递,如上文所述。

java.sql.Timestamp ts = java.sql.Timestamp.from( zdt.toInstant() );

从数据库中检索时,转换回纽约日期时间。从java.sql.Timestamp 转换为Instant,然后应用时区ZoneId 以获得ZonedDateTime

Instant instant = ts.toInstant();
ZoneId zoneId = ZoneId.of( "America/New_York" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

如果您的database driver 符合JDBC 4.2 或更高版本,您可以直接传递/获取java.time 类型,而不是转换为java.sql 类型/从java.sql 类型转换。试试PreparedStatement::setObjectResultSet::getObject 方法。

【讨论】:

  • 您建议为您的本地/测试机器做什么。就我而言,它只是一台普通计算机,因此将操作系统时间设置为 UTC 并没有任何意义。另一方面,如果我不将其操作系统时间设置为 UTC(具体来说,我注意到像 resultSet.getTimestamp() 这样的调用会根据系统时间进行调整),那么测试将在本地机器上产生不同(且不正确)的结果。我应该在ContextListener 中将TimeZone.setDefault() 设置为UTC 还是什么?
  • @theyuv 重新阅读我的答案,搜索 Stack Overflow,因为您的问题已被多次解决。快速:永远不要依赖服务器操作系统或 JVM 的默认时区,始终通过始终传递可选的 ZoneId 参数在 Java 代码中明确指定。切勿使用 java.sql 日期时间类,例如 Timestamp,仅使用带有 JDBC 4.2 或更高版本驱动程序的 java.time 类。并且 从不 调用 TimeZone.setDefault,因为这会在运行时立即影响(!)在该 JVM 内所有应用程序的所有线程中运行的所有代码。为了进行测试,请研究该类中内置的备用 Clock 实现。
【解决方案2】:

使用带有本地时区的时间戳
如果您希望数据库自动 在数据库和会话时区之间转换时间。

以最多 9 位小数精度存储日期和时间。这个数据类型是 对时区差异敏感。此类型的值会自动转换 在数据库时区和本地(会话)时区之间。当值是 存储在数据库中,它们被转换为数据库时区,但本地 (会话)时区不存储。当从数据库中检索一个值时, 值从数据库时区转换为本地(会话)时区。

【讨论】:

  • 就像添加:TIMESTAMP WITH LOCAL TIME ZONE总是显示当前用户会话时区的时间,不需要转换。
  • @)Albert Einstein 1) 我正在使用 JDBC 连接 oracle DB。如何将用户会话时区设置为数据库连接。
  • @Albert Einstein 2) 日期提取正常。 Oracle 将负责转换。如何处理日期保存。我有很多带有用户输入日期的屏幕(没有时间)。
  • @Gopi 我是一个预言机新手,我不能很好地回答你的其他问题,但是我已经为此开始了赏金,希望你能得到解决。
  • @Albert Einstein 感谢您为这个问题开始赏金。希望我能得到解决方案。我正在寻找上个月的解决方案。
【解决方案3】:

这里我已经澄清了以下具体问题。

问。我们大部分时间都是单独捕获日期,是否需要始终捕获日期和时间以及时区?

答。是的

Q.在存储日期和时间时我们需要在javascript/java/oracle中将用户本地时区转换为UTC?

A.数据保存时不转换,以源日期+时间+时区保存

Q. 在获取我们需要将 UTC 转换为用户的日期和时间时 本地时区查询本身/java/java 脚本?

A. 始终转换为以本地时区或应用程序打开的 UTC 格式显示。

Q.我们在很多地方都有根据日期列显示的报告,例如 今天/当前月份/日期范围。我们如何处理这个(输入 - 用户本地时区 - UTC 数据库)?

A. 系统应为用户提供日期时间显示格式的设置选项,可以是应用程序打开的本地时间,也可以是 UTC。一切都只在前端完成。

问。我们必须为日期字段使用哪种数据类型(日期/时间戳/带有时区的时间戳/带有本地时区的时间戳)?

A. 时间戳

简而言之,将日期时间保存在源时区中,并根据用户偏好以打开页面的本地位置或 UTC 格式进行转换。意味着,转换将通过脚本完成,仅用于显示。也可以找到产品流行的地区。

【讨论】:

    【解决方案4】:

    我将简单地将存储在数据库中的现有日期转换为Long,并保持(ETL 过程)这个Long 值,以及已知(或扣除)patternLocaleTimeZone (默认元)。只要使用默认元数据,就可以保留任何新的Date

    ETL 示例

    假设2015-11-29 10:07:49.500 UTC 存储在数据库中:

    // Known or deducted format of the persisted date
    String   pattern = "yyyy-MM-dd HH:mm:ss.SSS"; 
    Locale   locale  = Locale.ENGLISH;
    TimeZone zone    = "UTC";
    
    // Date to ms
    SimpleDateFormat sdf = new SimpleDateFormat(pattern, locale);
    sdf.setTimeZone(TimeZone.getTimeZone(zone));
    Date date = sdf.parse(pattern);
    
    // ETL: Can now be persisted in Long, along with default META (pattern, Locale, TZ)
    Long dateL = date.getTime(); // for e.g. 1448827660720
    ...
    

    如果需要,还可以将持久化的 Long 值转换为任何其他格式

    pattern                  | locale   | tz   |  result
    ============================================
    yyyy/MM/dd               | null     | null |  2015/11/29          
    dd-M-yyyy hh:mm:ss       | null     | null |  29-11-2015 10:07:40
    dd MMMM yyyy zzzz        | ENGLISH  | null |  29 November 2015 Central European Time
    yyyy-MM-dd HH:mm:ss.SSS  | null     | UTC  |  2015-11-29 10:07:49 UTC
    

    【讨论】:

      【解决方案5】:

      可行且合乎逻辑的方法是;将用户输入的时间转换为 GMT/UTC +00 并将其存储在带有或不带有时区标识符的数据库中,这无关紧要。当您需要向用户显示时间时,将 java 中的 GMT/UTC 时间转换为用户本地时间。

      【讨论】:

        【解决方案6】:

        您应该考虑JodaTime 并应该遵循第一个建议。JodaTime 有许多类,例如 LocalDate、LocalDateTime,您可以将它们用于不同的用例。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-12-28
          • 2017-08-08
          • 2012-08-06
          • 2012-09-01
          • 1970-01-01
          • 1970-01-01
          • 2011-08-23
          • 2021-08-09
          相关资源
          最近更新 更多