我对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
DATE
数据类型)java.sql.Timestamp
代替java.sql.Date
答案 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.DATE
到java.sql.Date
的转换。
您提到多个RDBMS实现正确处理时区,但SQLite除外。让我们看一下当您将日期值发送到JDBC驱动程序以及从JDBC驱动程序获取日期值时H2和SQLite如何工作。
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使用ValueDate
。 ValueDate.getDate()
调用DateTimeUtils.convertDateValueToDate(long)
,最终在时区转换后创建java.sql.Date
。
使用此SQLite JDBC driver,RS.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)
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指定您可以通过我在代码段中使用的方法LocalDate
和setObject
(包括getObject
也setDate
)。
我认为几乎我们所有人现在都在使用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时区中创建相同的日期/时间。