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

Posted

技术标签:

【中文标题】SQL DATE 与 java.sql.Date 中的时区【英文标题】:Timezones in SQL DATE vs java.sql.Date 【发布时间】:2012-03-01 10:49:39 【问题描述】:

我对 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

【问题讨论】:

我不太确定,但我唯一看到的是,它在调用 getTime() 方法时被转换为当前时区。 @novice_at_work:怎么样? new java.sql.Date(0).getTime() 产生 0。那里没有时区问题...?转换必须在 JDBC 驱动程序或数据库中完成。 当你在上面说new Date(0)时,是java.sql.Date还是java.util.Date @AdamRofer:澄清。顺便说一句,没有PreparedStatement.setDate(int, java.util.Date) 方法... ;-) 【参考方案1】:

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 日期和时间 API java.time 的一部分。 JDBC 4.2 指定您可以通过我在 sn-p 中使用的方法 setObjectgetObject 将包括 LocalDate 在内的 java.time 对象传输到您的 SQL 数据库或从您的 SQL 数据库传输(所以不是 setDategetDate)。

我认为现在我们几乎所有人都在使用 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 tutorial: Date Time 解释如何使用 java.time。 JSR-000221 JDBC™ API Specification 4.2 Maintenance Release 2 Documentation of java.sql.Date

【讨论】:

【参考方案2】:

我认为时区问题过于复杂。当您将日期/时间传递给准备好的语句时,它只会将日期和/或时间信息发送到数据库服务器。根据事实上的标准,它不会操纵数据库服务器和 jvm 之间的时区差异。稍后当您从结果集中读取日期/时间时。它只是从数据库中读取日期时间信息,并在 jvm 时区创建相同的日期/时间。

【讨论】:

【参考方案3】:

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

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

您看到的差异是表示方式的差异,而不是时间的差异。

【讨论】:

感谢您的回答。从本质上讲,这是已经说过的内容的简短版本 请注意,ResultSet.getDate() 生成 java.sql.Date 而不是 java.util.Date。另请注意,java.sql.Date 被定义为将其内部时间戳截断为格林威治标准时间午夜,因此如果给它的时间位于非格林威治标准时间的时区,结果可能会有所不同。【参考方案4】:

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

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

Statement.getTime、.getDate 和 .getTimestamp 方法通过 Calendar 对象将时区旋转到错误的方向。

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

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

所以,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 除外。让我们看看H2 和SQLite 在向 JDBC 驱动程序发送日期值以及从 JDBC 驱动程序获取日期值时是如何工作的。

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

使用这个SQLite JDBC driver,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 driver,RS.getDate(int)的代码就简单多了;它只是使用存储在数据库中的long 日期值返回一个java.sql.Date

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

虽然 JDBC 规范没有详细说明时区处理,但 RDBMS 和 JDBC 实现设计者将时区考虑在内并妥善处理是很有意义的;特别是如果他们希望他们的产品在全球范围内销售。这些设计师非常聪明,即使在没有具体规范的情况下,他们中的大多数人都能做到这一点,我并不感到惊讶。


我发现了这个 Microsoft SQL Server 博客 Using time zone data in SQL Server 2008,它解释了时区如何使事情复杂化:

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

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

【讨论】:

所以本质上,您是说我的所有 11 个测试数据库(SQLite 除外)的行为方式都相同的事实仅仅是巧合,或“偶然的兼容性”,并没有具体说明?我希望不是!?但是,我同意你的理由,你可能也是对的,但我觉得这有点可怕...... :-) 天啊,我越来越不喜欢你的帖子了。给我带来一些好消息! ;-) 不幸的是,当您实际上必须处理多个时区时,日期/时间 RDBMS 类型可能会非常痛苦。尤其是当您将遗留应用程序混入其中时......而且它们不处理时区。 SQL Server JDBC 4.0/4.1 驱动程序调整datetime 列值,就好像数据库保持本地到客户端时间一样。如果您实际上假设 DB 持有 UTC 值,这会导致错误。在***.com/a/29705750/173149 上查看我的调查@ 全局设置TimeZone.setDefault(TimeZone.getTimeZone("GMT+00:00")); 帮助使用 UTC 中的数据库日期。【参考方案5】:

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 中,则日期将为 1970-01-01(X>=0)和 1969-12-31(X


旁注:很奇怪Date(long) constructor 的文档与实现不同。 javadoc 是这样说的:

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

然而在 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?

【讨论】:

感谢您的回复。有点忽略了类级别的Javadoc,和构造函数Javadoc有点矛盾……【参考方案6】:

有一个getDate 的重载接受Calendar,使用它可以解决您的问题吗?或者您可以使用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会返回当前的语言环境,否则你将不得不查找如何进行快速时区转换。

【讨论】:

问题实际上并不是要绕过这个“问题”,而是要理解这个看似没有文档记录的 JDBC“特性”的基本原理。所以不幸的是,我不知道检索还是存储是“问题”...... @LukasEder:我会说两者。 JDBC 应该统一地从数据库中存储和检索信息。要么假设 GMT,要么假设本地时区,不要做一个然后另一个。似乎 new Date(0)getDate 正在分配本地时区,而另一个正在使用 GMT。【参考方案7】:

进行转换的是 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。因此,与原始日期相比,您“损失”了一个小时。

【讨论】:

这是最可能的解释,是的 @philwb 你能告诉我应该使用哪个 JDBC 驱动程序吗?请...现在我正在使用 com.mysql.cj.jdbc.Driver

以上是关于SQL DATE 与 java.sql.Date 中的时区的主要内容,如果未能解决你的问题,请参考以下文章

java.sql.Date与java.sql.Date区别

java.util.Date 与 java.sql.Date

util.date和sql.date的衔接处理

java.util.Date和java.sql.Date的区别及应用

java.sql.Date和java.util.Date的区别

java.util.Date和java.sql.Date以及System.currentTimeMillis()涉及到时间的问题