SQL DATE 与 java.sql.Date 中的时区



我对SQL DATE数据类型的行为与java.sql.Date的行为感到有些困惑。以以下语句为例:

select cast(? as date)           -- in most databases
select cast(? as date) from dual -- in Oracle

让我们使用 Java 准备并执行语句

PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setDate(1, new java.sql.Date(0)); // GMT 1970-01-01 00:00:00
ResultSet rs = stmt.executeQuery();
rs.next();
// I live in Zurich, which is CET, not GMT. So the following prints -3600000, 
// which is CET 1970-01-01 00:00:00
// ... or   GMT 1969-12-31 23:00:00
System.out.println(rs.getDate(1).getTime());

换句话说,我绑定到语句的 GMT 时间戳将成为我返回的 CET 时间戳。时区是在哪一步添加的,为什么?

注意:

  • 我观察到这些数据库中的任何一个都是如此:

    DB2, Derby, H2, HSQLDB, Ingres, MySQL, Oracle, Postgres, SQL Server, Sybase ASE, Sybase SQL Anywhere

  • 我观察到这对于SQLite来说是错误的(它实际上没有真正的DATE数据类型(
  • 当使用java.sql.Timestamp而不是java.sql.Date时,所有这些都无关紧要
  • 这是一个类似的问题,但是没有回答这个问题:java.util.Date vs java.sql.Date

JDBC 规范没有定义有关时区的任何细节。尽管如此,我们大多数人都知道必须处理 JDBC 时区差异的痛苦;看看所有的StackOverflow问题!

最终,日期/时间数据库类型的时区处理归结为数据库服务器、JDBC 驱动程序以及介于两者之间的所有内容。您甚至会受到 JDBC 驱动程序错误的摆布;PostgreSQL 修复了 8.3 版中的一个错误,其中

传递日历对象的 Statement.getTime、.getDate 和 .getTimestamp 方法以错误的方向旋转时区。

当您使用 new Date(0) 创建新日期时(假设您使用的是 Oracle JavaSE java.sql.Date ,您的日期已创建

使用给定的毫秒时间值。如果给定的毫秒值包含时间信息,则驱动程序会将时间部分设置为默认时区(运行应用程序的 Java 虚拟机的时区(中对应于零 GMT 的时间。

因此,new Date(0)应该使用GMT。

当您调用 ResultSet.getDate(int) 时,您正在执行 JDBC 实现。JDBC 规范没有规定 JDBC 实现应如何处理时区详细信息;因此,您可以受制于实施。查看Oracle 11g oracle.sql.DATE JavaDoc,Oracle DB似乎不存储时区信息,因此它执行自己的转换以将日期转换为java.sql.Date。我没有使用 Oracle DB 的经验,但我猜 JDBC 实现正在使用服务器和本地 JVM 的时区设置来执行从 oracle.sql.DATEjava.sql.Date 的转换。


您提到多个RDBMS实现可以正确处理时区,SQLite除外。让我们看看当您将日期值发送到 JDBC 驱动程序以及从 JDBC 驱动程序获取日期值时,H2 和 SQLite 是如何工作的。

H2 JDBC 驱动程序PrepStmt.setDate(int, Date)使用 ValueDate.get(Date) ,它调用执行时区转换的DateTimeUtils.dateValueFromDate(long)

使用此 SQLite JDBC 驱动程序,PrepStmt.setDate(int, Date)调用PrepStmt.setObject(int, Object),并且不执行任何时区转换。

H2 JDBC 驱动程序JdbcResultSet.getDate(int)返回get(columnIndex).getDate()get(int)返回指定列的 H2 Value。由于列类型是DATE,H2 使用 ValueDateValueDate.getDate()调用DateTimeUtils.convertDateValueToDate(long),最终在时区转换后创建一个java.sql.Date

使用此SQLite JDBC驱动程序,RS.getDate(int)代码要简单得多;它只是使用存储在数据库中的long日期值返回java.sql.Date

因此,我们看到 H2 JDBC 驱动程序在处理带有日期的时区转换方面很聪明,而 SQLite JDBC 驱动程序则不然(并不是说这个决定不聪明,它可能非常适合 SQLite 设计决策(。如果你追寻你提到的其他RDBMS JDBC驱动程序的来源,你可能会发现大多数都以与H2类似的方式接近日期和时间区。

虽然JDBC

规范没有详细说明时区处理,但RDBMS和JDBC实现设计人员考虑时区并正确处理时区是有道理的;特别是如果他们希望他们的产品在全球舞台上销售。这些设计师非常聪明,即使没有具体的规范,他们中的大多数人也能做到这一点,我并不感到惊讶。


我在SQL Server博客中使用时区数据Microsoft SQL Server 2008中找到了这个,该博客解释了时区如何使事情复杂化:

时区

是一个复杂的领域,每个应用程序都需要解决如何处理时区数据的问题,以使程序更加用户友好。

不幸的是,目前没有时区名称和值的国际标准权威机构。 每个系统都需要使用自己选择的系统,在有国际标准之前,尝试让SQL Server提供一个是不可行的,最终会导致比它解决的问题更多的问题。

执行

转换的是 jdbc 驱动程序。 它需要将 Date 对象转换为 db/wire 格式可接受的格式,当该格式不包含时区时,在解释日期时倾向于默认为本地计算机的时区设置。 因此,鉴于您指定的驱动程序列表,最有可能的情况是将日期设置为 GMT 1970-1-1 00:00:00,但将其设置为语句时的解释日期为 CET 1970-1-1 1:00:00。 由于日期只是日期部分,因此您会将 1970-1-1(没有时区(发送到服务器并回显给您。 当驱动程序获取日期,并且您将其作为日期访问时,它会看到 1970-1-1 并使用本地时区再次解释该时区,即 CET 1970-1-1 00:00:00 或 GMT 1969-12-31 23:00:00。 因此,与原始日期相比,您"损失"了一个小时。

java.util.Date 和 Oracle/MySQL Date 对象都只是时间点的表示,与位置无关。这意味着它很可能在内部存储为自"纪元"(GMT 1970-01-01 00:00:00(以来的毫/纳秒数。

当您从结果集中读取时,对"rs.getDate(("的调用会告诉结果集获取包含时间点的内部数据,并将其转换为 java Date 对象。此日期对象是在本地计算机上创建的,因此 Java 将选择您的本地时区,即苏黎世的 CET。

你看到的差异是表示的差异,而不是时间的差异。

java.sql.Date对应于不存储时间或时区信息的 SQL DATE。完成此操作的方式是通过"规范化"日期,就像javadoc所说的那样:

为了符合 SQL DATE 的定义,必须通过将与实例关联的特定时区中的小时、分钟、秒和毫秒设置为零来"规范化"由 java.sql.Date 实例包装的毫秒值。

这意味着,当您在 UTC+1 中工作并要求数据库提供 DATE 时,合规的实现完全按照您观察到的方式执行:返回一个 java.sql.Date,其毫秒值对应于 00:00:00 UTC+1 的相关日期,与数据最初到达数据库的方式无关。

如果不是您想要的,数据库驱动程序可能允许通过选项更改此行为。

另一方面,当您将java.sql.Date传递给数据库时,驱动程序将使用默认时区将日期和时间部分与毫秒值分开。如果您使用 0 并且使用的是 UTC+X,则 X>=0 的日期为 1970-01-01,X<0 的日期为 1969-12-31。


旁注:奇怪的是,Date(long)构造函数的文档与实现不同。javadoc 是这样说的:

如果给定的毫秒值包含时间信息,则驱动程序会将时间部分设置为默认时区(运行应用程序的 Java 虚拟机的时区(中对应于零 GMT 的时间。

然而,在OpenJDK中实际实现的是这样的:

public Date(long date) {
    // If the millisecond date value contains time info, mask it out.
    super(date);
}

显然,这种"掩盖"没有实施。同样,因为指定的行为没有很好地指定,例如 1970-01-01 00:00:00 GMT-6

= 1970-01-01 06:00:00 GMT 应该映射到 1970-01-01 00:00:00 GMT = 1969-12-31 18:00:00 GMT-6,还是映射到 1970-01-01 18:00:00 GMT-6?

JDBC 4.2 和 java.time

PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setObject(1, LocalDate.EPOCH); // The epoch year LocalDate, '1970-01-01'.
ResultSet rs = stmt.executeQuery();
rs.next();
// It doesnt matter where you live or your JVM’s time zone setting
// since a LocalDate doesn’t have or use time zone
System.out.println(rs.getObject(1, LocalDate.class));

我没有测试,但除非您的 JDBC 驱动程序中存在错误,否则所有时区的输出都是:

1970年01月01

LocalDate是一个没有一天中时间和时区的日期。因此,没有毫秒值可以获取,也没有一天中的时间可以混淆。 LocalDate是现代Java日期和时间APIjava.time的一部分。JDBC 4.2 指定您可以通过我在代码段中使用的方法setObjectgetObject将 java.time 对象(包括LocalDate(传输到 SQL 数据库或从 SQL 数据库传输 java.time 对象(因此既不是setDate也不是getDate(。

我认为现在几乎所有人都在使用 JDBC 4.2 驱动程序。

您的代码中出了什么问题?

java.sql.Date是一个黑客,试图(不是很成功(将java.util.Date伪装成没有一天中时间的日期。我建议您不要使用该类。

从文档中:

为了符合 SQL DATE的定义,毫秒值 由java.sql.Date实例包装必须通过设置 "规范化" 小时、分钟、秒和毫秒到零 与实例关联的特定时区。

"关联"可能含糊不清。我相信这意味着毫秒值必须表示 JVM 默认时区(我想是欧洲/苏黎世(的一天的开始。因此,当您创建等于 GMT 1970-01-01 00:00:00 的new java.sql.Date(0)时,实际上是您创建了一个不正确的Date对象,因为此时此时间是您所在时区的 01:00:00。JDBC 忽略了一天中的时间(我们可能预料到会有错误消息,但没有得到任何错误消息(。因此,SQL 获取并返回的日期为 1970-01-01。JDBC 将其正确转换回包含 CET 1970-01-01 00:00:00 的Date。或毫秒值 -3 600 000。是的,这令人困惑。正如我所说,不要使用该类。

如果您处于负 GMT 偏移量(例如美洲的大多数时区(,new java.sql.Date(0)将被解释为 1969-12-31,因此这将是您传递到并从 SQL 返回的日期。

更糟糕的是,想想当JVM的时区设置被更改时会发生什么。程序的任何部分和在同一 JVM 中运行的任何其他程序都可以随时执行此操作。这通常会导致在更改之前创建的所有对象java.sql.Date无效,并且有时可能会将其日期更改一天(在极少数情况下会更改两天(。

链接

  • Oracle 教程:日期时间解释如何使用 java.time。
  • JSR-000221 JDBC™ API 规范 4.2 维护版本 2
  • java.sql.Date文档

有一个接受CalendargetDate重载,使用它可以解决你的问题吗?或者,您可以使用 Calendar 对象来转换信息。

也就是说,假设检索是问题所在。

Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setDate(1, new Date(0)); // I assume this is always GMT
ResultSet rs = stmt.executeQuery();
rs.next();
//This will output 0 as expected
System.out.println(rs.getDate(1, gmt).getTime());

或者,假设存储是问题所在。

Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
PreparedStatement stmt = connection.prepareStatement(sql);
gmt.setTimeInMillis(0) ;
stmt.setDate(1, gmt.getTime()); //getTime returns a Date object
ResultSet rs = stmt.executeQuery();
rs.next();
//This will output 0 as expected
System.out.println(rs.getDate(1).getTime());

我没有这方面的测试环境,所以你必须找出哪个是正确的假设。另外,我相信getTime返回当前区域设置,否则您将不得不查找如何进行快速时区转换。

相关内容

  • 没有找到相关文章

最新更新