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

2022-09-01 03:44:46

我对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.Timestampjava.sql.Date
  • 这是一个类似的问题,但是没有回答这个问题:java.util.Date vs java.sql.Date

答案 1

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

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

传递 Calendar 对象的 Statement.getTime、.getDate 和 .getTimestamp 方法正在向错误的方向旋转时区。

当您使用(让我们断言您正在使用 Oracle JavaSE java.sql.Date)创建新日期时,您的日期即被创建new Date(0)

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

所以,应该使用GMT。new Date(0)

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


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

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(int) 返回指定列的 H2 。由于列类型为 ,H2 使用值日期ValueDate.getDate() 调用 DateTimeUtils.convertDateValueToDate(long),这最终会在时区转换后创建一个转换。get(columnIndex).getDate()DATEjava.sql.Date

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

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

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


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

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

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


答案 2

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