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

时间:2012-02-08 22:58:13

标签: java sql date jdbc timezone

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

7 个答案:

答案 0 :(得分:38)

JDBC specification未定义与时区有关的任何详细信息。尽管如此,我们大多数人都知道必须处理JDBC时区差异的痛苦;只需查看所有StackOverflow questions

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

  

传递Calendar对象的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驱动程序获取日期值时H2SQLite如何工作。

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

使用此SQLite JDBC driverPrepStmt.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 driverRS.getDate(int)代码更简单;它只使用存储在数据库中的java.sql.Date日期值返回long

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

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


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

  

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

     

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

答案 1 :(得分:10)

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

答案 2 :(得分:5)

java.util.Date和Oracle / MySQL Date对象都只是一个时间点的表示,而不管位置如何。这意味着它很可能在内部存储为自“epoch”GMT 1970-01-01 00:00:00以来的毫秒/纳秒数。

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

您所看到的差异是代表性的差异,而不是时间的差异。

答案 3 :(得分:3)

java.sql.Date对应于SQL DATE,它不存储时间或时区信息。实现这一目标的方法是“标准化”日期,就像javadoc所说的那样:

  

为了符合SQL DATE的定义,java.sql.Date实例包含的毫秒值必须通过在特定时区中将小时,分钟,秒和毫秒设置为零来“标准化”。实例是关联的。

这意味着当你在UTC + 1工作并向数据库询问DATE时,一个兼容的实现正是你所观察到的:返回一个java.sql.Date,其毫秒值对应于有问题的日期< em> at 00:00:00 UTC + 1 ,与数据首先进入数据库的方式无关。

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

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


旁注:看到Date(long) constructor的文档与实现不同,这很奇怪。 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?

答案 4 :(得分:2)

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会返回当前的区域设置,否则您将需要查找如何进行快速时区转换。

答案 5 :(得分: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指定您可以通过我在代码段中使用的方法LocalDatesetObject(包括getObjectsetDate)。

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

您的代码出了什么问题?

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

从文档中:

  

要符合SQL java.util.Date的定义,毫秒值   由DATE实例包装的对象必须通过设置进行“规范化”   在小时,分钟,秒和毫秒中将零   实例所关联的特定时区。

“关联”可能含糊不清。我相信,这意味着毫秒值必须表示JVM默认时区(假设是欧洲/苏黎世)中的一天的开始。因此,当您创建等于格林尼治标准时间1970-01-01 00:00:00的java.sql.Date时,实际上是您创建了错误的new java.sql.Date(0)对象,因为该时间在您的时区中是01:00:00。 JDBC忽略了一天中的时间(我们可能希望收到一条错误消息,但什么也没得到)。因此,SQL获取并返回1970-01-01日期。 JDBC将其正确地转换回包含CET 1970-01-01 00:00:00的Date。或毫秒值-3 600000。是的,令人困惑。如我所说,不要使用该类。

如果您的格林尼治标准时间偏移为负(例如,美洲的大多数时区),Date将被解释为1969-12-31,因此这将是您传递给的日期并从SQL回来。

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

链接

答案 6 :(得分:0)

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