我正在开发一个应用程序,用于存储一些带有过去时间戳的数据。
我的 JVM 是 IBM Semeru (17),运行于 Europe/Paris tz。但我想将时间戳存储在 UTC (GMT+0)。
下面是我的代码:
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.1</version>
</dependency>
</dependencies>
public class Main {
/*
create table theentity (
theid integer not null,
thevalue timestamp(6),
primary key (theid)
)
*/
public static void main(String[] args) {
TimeZone.setDefault( TimeZone.getTimeZone( ZoneId.of( "Europe/Paris" ) ) );
LocalDateTime d_1900_01_01_T_00_09_23 = LocalDateTime.of( 1900, 1, 1, 0, 9, 23, 0 );
LocalDateTime d_1900_01_01_T_00_09_22 = LocalDateTime.of( 1900, 1, 1, 0, 9, 22, 0 );
LocalDateTime d_1900_01_01_T_00_09_21 = LocalDateTime.of( 1900, 1, 1, 0, 9, 21, 0 );
LocalDateTime d_1900_01_01_T_00_09_20 = LocalDateTime.of( 1900, 1, 1, 0, 9, 20, 0 );
LocalDateTime d_1900_01_01_T_00_09_19 = LocalDateTime.of( 1900, 1, 1, 0, 9, 19, 0 );
try(Connection c = DriverManager.getConnection( "jdbc:postgresql://localhost:5432/hibernate_orm_test?preparedStatementCacheQueries=0&escapeSyntaxCallMode=callIfNoReturn",
"postgres", "root")) {
PreparedStatement p = c.prepareStatement( "insert into theentity values(?, ?)" );
bindAndExecute( p, 1, d_1900_01_01_T_00_09_23 );
bindAndExecute( p, 2, d_1900_01_01_T_00_09_22 );
bindAndExecute( p, 3, d_1900_01_01_T_00_09_21 );
bindAndExecute( p, 4, d_1900_01_01_T_00_09_20 );
bindAndExecute( p, 5, d_1900_01_01_T_00_09_19 );
} catch (Exception e) {
e.printStackTrace();
}
}
private static void bindAndExecute(PreparedStatement p, int id, LocalDateTime localDateTime)
throws SQLException {
p.setInt( 1, id );
p.setTimestamp(2,
Timestamp.valueOf( localDateTime ),
Calendar.getInstance( TimeZone.getTimeZone( ZoneId.of( "GMT" ) ) )
);
p.executeUpdate();
}
}
为了排序,我尝试保留 5 个时间戳(请注意,这些时间戳基于欧洲/巴黎,因为TimeZone.setDefault( TimeZone.getTimeZone( ZoneId.of( "Europe/Paris" ) ) );
)
- 1900-01-01T00:09:23
- 1900-01-01T00:09:22
- 1900-01-01T00:09:21
- 1900-01-01T00:09:20
- 1900-01-01T00:09:19
这行代码将把 Europe/Paris tz 翻译为 UTC tz:
p.setTimestamp(2,
Timestamp.valueOf( localDateTime ),
Calendar.getInstance( TimeZone.getTimeZone( ZoneId.of( "GMT" ) ) ) );
执行它将在postresql中创建5行:
theid | thevalue
-------+---------------------
1 | 1900-01-01 00:00:02
2 | 1900-01-01 00:00:01
3 | 1900-01-01 00:00:00
4 | 1899-12-31 23:09:20
5 | 1899-12-31 23:09:19
(5 rows)
你们大多数人可能认为巴黎位于 GMT+1。这是正确的,但实际上并不正确。1900 年,巴黎位于 GMT+00:09:21(9 分 21 秒)!!
因此,前 3 个时间戳已正确保存到表中。但是,奇怪的是在第 4 行。它是,1899-12-31 23:09:20
但我预计它会是1899-12-31 23:59:59
。但似乎在 1899 年,旧的 DateTime API 认为它是 GMT+1。这导致第 4、5 行等等……都是错误的!!
你们能解释一下这个吗?
您一周前问过类似的问题,我在该问题的评论中提到了这个问题。
根本解释是 tzdata 混淆和 OpenJDK 错误。
1970 年之前,OpenJDK 时区完全被破坏了。也就是说,没有任何保证,而且目前还没有解决办法:OpenJDK 项目不愿意了解 tzdata 的政策是如何改变的,或者不愿意支持 1970 年之前的正确时间,即使他们了解 tzdata 项目的 2022b 更新意味着什么。
什么是 tzdata?
tzdata 是一个独立的项目,被很多东西使用,包括 Linux 内核和 JDK;该项目是一个文件,其中包含大量时区的精确定义以及它们随时间如何变化和何时变化的历史数据。
鉴于历史上很难准确确定一个国家在遥远过去任何特定时刻的时区,tzdata 的基本集不再保证任何内容,如果它是关于 1970 年之前的时区信息。我猜,出于历史原因,它仍然提供了一堆 1970 年之前的定义和更改,但不能保证这些是正确的。事实上,它们通常不是:过于简单,通常是已知的错误内容。我猜有些人试图在工作中兼顾简单性和减少 tz 表的大小,至少在一定程度上保持准确性。
这不是 tzdata 的错;tzdata 工作方式的这种转变的核心原则本质上是正确的:不可能高度确定 1970 年之前的时区数据;在开源极其有限的预算下,可以轻松验证的记录太少了(tzdata 是一个开源项目,或多或少由一个人运行;他们不能飞到巴黎检查国家图书馆或类似机构的一些缩微胶卷资料),而且关于哪些地方存在过的不同意见确实变得非常混乱。我们应该有一个吗
Europe/Vichy
?那Prussia/Koningsberg
(第一次世界大战前统一的德国的一部分,稍稍易手,现在是俄罗斯的现代加里宁格勒?)但是,在 2022 年左右之前,tzdata 尝试做到一切,并尽可能准确。但是,从那时起,就不再如此了:相反,有一个扩展的 tzdata 文件,其“验证”程度明显较低(即,如果您尝试针对某些模糊的几个月提交针对扩展数据的错误,根据难以验证的证据,可以说在一段时间内使用了不同的时区,即使总体而言它更有可能是真的,它也可能会被忽略,并且肯定不会被优先考虑)。
但是,该扩展文件与 2022 年的 tzdata 完全一致。
OpenJDK 仅具有基础(仅在 1970 年后验证)tzdata 文件;它不包含扩展定义。
因此,错误:1970 年之前的欧洲/巴黎可能不正确。我确信 1938-1945 年范围内的欧洲/阿姆斯特丹不正确。
这看起来像什么
使用基于 tzdata 2022a 版本或更早版本的 JDK,此代码:
这会在“正确”的 JDK 上打印 -1022980800000L(目前,相当老旧的版本,在 2022 年之前构建)。就历史爱好者而言,这是正确的时间。
但在现代 JDK 上,它不再打印该数字;不再打印。自从 JDK 集成 tzdata 2022-b(第一个简化了 1970 年之前内容的版本)以来,您就会得到不同的错误数字。
这对您的软件的影响取决于您如何使用日期,或者您的部门如何使用它。
举一个具体的例子,H2(数据库引擎)会完全搞砸。如果你存储一个日期,即使你使用强烈推荐的策略(即
preparedStatement.setObject(LocalDate.of(1937, 8, 2))
使用推荐的写入和读取策略resultSet.getObject(colIdx, LocalDate.class)
)(JDBC 规范保证它可以在任何兼容的 JDBC 实现上运行),你在 2022b 之前的 JDK 上写入的日期,在 2022b 之后的 JDK 上读回的日期也会返回一个错误的日期。1937 年是一个完全合理的出生年份。出生日期在许多情况下(尤其是医疗情况下)都是关键因素,因此这打破了一切。
修复 - 步骤 1
您需要做两件事来修复您的 JDK。第 1 步是修复 tzdata 文件:
oracle 提供了tzupdater 工具,可让您重写 JDK 使用的 tzdata 文件。下载该工具并在 posix 系统上按如下方式使用它:
请注意,这意味着通过切换到此 def,可以删除自 2022 年以来的任何 tz 更新(嘿,政治实体有时会下令更改时区,这种情况确实会发生);2022a 是该集合最后一次是“1970 年之前的最佳努力”。如果您此时还需要任何 2022a 之后的更改,我没有解决方案。
这将更新 JDK 安装,该安装提供您运行该命令所需的 java 可执行文件。当然,您需要管理员权限,因为 JDK 需要重写自己的文件才能执行此操作。
第 2 步 -非常小心日期到毫秒的转换
正确的 DB 设计将存储 LocalDate 作为其组成字段:日期 1937-08-02 应按如下方式存储:存储这 3 个数字。使用一些位操作将它们推入尽可能少的位中,但不幸的是,这不是 H2 v1 所做的;相反,它使用一些规定的时区将其转换为午夜该日期的纪元毫秒,存储该数据,并在读出该数据时,以相反的方式执行相同的操作。这就是为什么即使您同时使用
LocalDate
“in”和“out”,H2 也会返回错误日期的原因。例如,即使您在 2022a 的 JDK 上“写入”数据并在 2022b 或更高版本上“读取”数据,psql 也不会这样做并返回稳定的日期。
但是,非常重要的一点是:
java.sql.Timestamp
和尤其java.sql.Date
是完全损坏的,您不能使用它们。不幸的是,许多抽象比如 JPA impls 经常这样,我不知道如何解决这个问题。问题是这两种类型扩展了java.util.Date
并且该类已损坏(这就是它被弃用的原因):这是一个谎言。它不代表日期,它代表时刻。这就是为什么所有与日期相关的方法(例如date.getYear()
)都被弃用的原因:因为它们给出的答案可能是错误的,而且无法修复。并且因为java.sql.Date
和java.sql.Timestamp
扩展了这个损坏的类,所以它们也损坏了。使用数据库正确处理日期的唯一方法是祈祷你的数据库正确存储它(不幸的是 H2 v1 没有这样做),并且你使用正确的方式将日期/时间数据传递到数据库和从数据库传出:
写:
您可以传递任何类型的实例
LocalDate
,,,或。不幸的是,最合适的日期/时间类型是,而据我所知,LocalDateTime
任何数据库都不支持它,或者没有正确支持它(我猜他们不知道如何将冗长的内容分成几位,他们觉得这很难存储是有道理的)。Instant
OffsetDateTime
ZonedDateTime
Europe/Paris
阅读:
您也可以使用
Instant
或LocalDateTime
,或OffsetDateTime
代替LocalDate
。为什么没有
.getLocalDate
像那里那样的getDate
?因为 JDBC 小组决定停止为
getType()
作为基线 DB <-> java 类型的每个新类型添加方法;以后再也不会创建此类方法。是getObject
的。相反,JDBC 规范已开始规定某些类型“必须受支持”。LocalDate 和上面提到的其他类型都在列表中。ZonedDateTime 不在。当然,您自己也绝不能将“人工计算”(以年、月、时、周等形式表示的时间)转换为“计算机计算”(纪元毫秒)。或者,至少,绝不能只转换一次然后再转换回来,因为该操作是不可逆的,即使您认为可以。“将巴黎的这个年/月/日时:分:秒值转换为纪元毫秒……然后明天我会要求您将其转换回来”实际上并不能保证给您相同的值。所以,不要这样做。将您的日期/时间内容原封不动地存储起来。不要认为
Instant
+TimeZone
可以完美地来回转换为ZonedDateTime
实例。因为 tzdata 可能会改变。小心自动更新!
如果您的服务器平台更新其 JDK 版本,您的 tzdata 更新将被覆盖。
我建议您在启动 Java 应用程序时运行此代码,如果检测到 tzdata 不是最新的,则立即硬退出;此时实际运行将意味着您的应用程序开始破坏您的数据库,而您不希望出现这种情况: