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

2022-08-31 20:19:57

任何 Java 8 + JDBC 专家都可以告诉我以下推理中是否有问题?而且,如果在神的秘密中,为什么没有这样做?

A 是 JDBC 当前用于映射到 DATE SQL 类型的类型,该类型表示不带时间和时区的日期。但是这个类的设计很糟糕,因为它实际上是 的子类,它存储了精确的瞬间,直到毫秒。java.sql.Datejava.util.Date

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

Java 8 引入了一个 LocalDate 类,该类更适合 DATE 数据库类型,因为它不是一个精确的时刻,因此不依赖于时区。Java 8还引入了默认方法,这些方法允许对ReadkEdStatement和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();
}

当然,同样的理由也适用于对 TIMESTAMP 类型的即时支持,以及对 TIME 类型的 LocalTime 的支持。


答案 1

因此,为了在数据库中表示日期2015-09-13,我们被迫选择一个时区,在该时区中解析字符串“2015-09-13T00:00:00.000”作为java.util.Date以获取毫秒值,然后从该毫秒值构造java.sql.Date,最后在预准备的语句上调用setDate(),传递一个保存所选时区的日历,以便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.Date对java.util.Date的依赖性,以及它因此通过Date(long)Date.getTime()继承java.time.Instant的语义的事实。

那么,我们难道没有错过一个巨大的机会来清理JDBC中的混乱,同时仍然保持向后兼容性吗?[...]

这要视情况而定。JDBC 4.2 规范指定您可以通过 setObject(int, localDate) 绑定类型,并通过 getObject(int, LocalDate.class) 获取类型(如果驱动程序已准备就绪)。当然,不像你建议的那样,像你建议的那样优雅。LocalDateLocalDatedefault


答案 2

简短的回答:不,日期时间处理在Java 8 / JDBC 4.2中是固定的,因为它支持Java 8日期和时间类型。这不能使用默认方法模拟。

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

这是不可能的,原因如下:

  • java.time.OffsetDateTime在 中不等效java.sql
  • java.time.LocalDateTime转换时 DST 转换中断,因为后者取决于 JVM 时区,请参阅下面的代码java.sql.Timestamp
  • java.sql.Time具有毫秒级分辨率,但具有纳秒级分辨率java.time.LocalTime

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

如果在具有夏令时的 JVM 时区运行此代码,它将中断。它搜索时钟“向前设置”的下一个转换,并选择一个正好处于转换中间的转换。这是完全有效的,因为Java或SQL没有时区,因此没有时区规则,因此没有夏令时。 另一方面绑定到JVM时区,因此受夏令时的限制。LocalDateTimeLocalDateTimeTIMESTAMPjava.sql.Timestamp

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());