错过了在Java 8中修复JDBC日期处理的机会?

时间:2015-09-13 09:43:40

标签: java date jdbc java-8

任何Java 8 + JDBC专家都能告诉我以下推理中是否存在错误?而且,如果在众神的秘密中,为什么还没有完成呢?

java.sql.Date当前是JDBC用于映射到DATE SQL类型的类型,它表示没有时间且没有时区的日期。但是这个类设计得很好,因为它实际上是java.util.Date的子类,它可以存储精确的瞬间,直到毫秒。

为了在数据库中表示日期2015-09-13,我们因此被迫选择时区,解析字符串" 2015-09-13T00:00:00.000"在该时区作为java.util.Date得到一个毫秒值,然后从这个毫秒值构造一个java.sql.Date,最后在准备好的语句上调用setDate(),传递一个包含所选时区的日历命令JDBC驱动程序能够从此毫秒值正确地重新计算日期2015-09-13。通过在任何地方使用默认时区,而不是传递日历,这个过程会变得更简单。

Java 8引入了一个LocalDate类,它更适合DATE数据库类型,因为它不是一个精确的时刻,因此不依赖于时区。 Java 8还引入了默认方法,这些方法允许对PreparedStatement和ResultSet接口进行向后兼容的更改。

那么,我们是否错过了在保持向后兼容性的同时清理JDBC中的混乱的巨大机会? Java 8可以简单地将这些默认方法添加到PreparedStatement和ResultSet:

default public void setLocalDate(int parameterIndex, LocalDate localDate) {
    if (localDate == null) {
        setDate(parameterIndex, null);
    }
    else {
        ZoneId utc = ZoneId.of("UTC");
        java.util.Date utilDate = java.util.Date.from(localDate.atStartOfDay(utc).toInstant());
        Date sqlDate = new Date(utilDate.getTime());
        setDate(parameterIndex, sqlDate, Calendar.getInstance(TimeZone.getTimeZone(utc)));
    }
}

default LocalDate getLocalDate(int parameterIndex) {
    ZoneId utc = ZoneId.of("UTC");
    Date sqlDate = getDate(parameterIndex, Calendar.getInstance(TimeZone.getTimeZone(utc)));
    if (sqlDate == null) {
        return null;
    }

    java.util.Date utilDate = new java.util.Date(sqlDate.getTime());
    return utilDate.toInstant().atZone(utc).toLocalDate();
}

当然,同样的推理适用于TIM对TIMESTAMP类型的Instant支持,以及LocalTime对TIME类型的支持。

2 个答案:

答案 0 :(得分:25)

  

为了在数据库中表示日期2015-09-13,我们因此被迫选择时区,将该时区中的字符串“2015-09-13T00:00:00.000”解析为java.util.Date得到一个毫秒值,然后从这个毫秒值构造一个java.sql.Date,最后在预准备语句上调用setDate(),传递一个包含所选时区的Calendar,以便JDBC驱动程序能够正确地重新计算日期2015-09-13来自这个毫秒值

为什么呢?只需致电

Date.valueOf("2015-09-13"); // From String
Date.valueOf(localDate);    // From java.time.LocalDate

所有JDBC驱动程序中的行为都是正确的:没有时区的本地日期。逆操作是:

date.toString();    // To String
date.toLocalDate(); // To java.time.LocalDate

你永远不应该依赖java.sql.Datejava.util.Date的依赖,以及它因此通过java.time.Instant或{{3}继承Date(long)的语义这一事实}

  

那么,难道我们还没有错过在清除后向兼容性的同时清理JDBC中的混乱的巨大机会吗? [...]

这取决于。 Date.getTime()指定您可以通过JDBC 4.2 spec绑定LocalDate类型,并通过setObject(int, localDate)获取LocalDate类型,如果驱动程序已准备好那。当然,正如您所建议的那样,不如更正式的default方法更优雅。

答案 1 :(得分:6)

简答:不,日期时间处理在Java 8 / JDBC 4.2中得到修复,因为它支持Java 8 Date and Time types。无法使用默认方法进行模拟。

  

Java 8可以简单地将这些默认方法添加到PreparedStatement和ResultSet:

由于以下几个原因,这是不可能的:

  • java.time.OffsetDateTimejava.sql
  • 中没有相同的内容 转换为java.time.LocalDateTime时,
  • java.sql.Timestamp在DST转换中断,因为后者取决于JVM时区,请参阅下面的代码
  • java.sql.Time具有毫秒分辨率,但java.time.LocalTime具有纳秒分辨率

因此,您需要正确的驱动程序支持,默认方法必须通过引入数据丢失的java.sql类型进行转换。

如果您在具有夏令时的JVM时区中运行此代码,它将会中断。它搜索时钟“向前”的下一个转换,并选择正好在转换过程中的LocalDateTime。这是完全有效的,因为Java LocalDateTime或SQL TIMESTAMP没有时区,因此没有时区规则,因此没有夏令时。另一方面,java.sql.Timestamp绑定到JVM时区,因此需要夏令时。

ZoneId systemTimezone = ZoneId.systemDefault();
Instant now = Instant.now();

ZoneRules rules = systemTimezone.getRules();
ZoneOffsetTransition transition = rules.nextTransition(now);
assertNotNull(transition);
if (!transition.getDateTimeBefore().isBefore(transition.getDateTimeAfter())) {
  transition = rules.nextTransition(transition.getInstant().plusSeconds(1L));
  assertNotNull(transition);
}

Duration gap = Duration.between(transition.getDateTimeBefore(), transition.getDateTimeAfter());
LocalDateTime betweenTransitions = transition.getDateTimeBefore().plus(gap.dividedBy(2L));

Timestamp timestamp = java.sql.Timestamp.valueOf(betweenTransitions);
assertEquals(betweenTransitions, timestamp.toLocalDateTime());