Отсутствует возможность исправить обработку данных 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()
для подготовленного оператора, передавая Calendar, содержащий часовой пояс, выбранный для того, чтобы драйвер 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();
}
Конечно, то же самое относится и к поддержке Instant для типа TIMESTAMP, и к поддержке LocalTime для типа TIME.
Ответы
Ответ 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
и тот факт, что это таким образом, наследует семантику java.time.Instant
через Date(long)
или Date.getTime()
Итак, разве мы не упустили огромную возможность убрать беспорядок в JDBC, сохраняя при этом обратную совместимость? [...]
Это зависит. Спецификация JDBC 4.2 специфицирует, что вы можете привязать тип LocalDate
через setObject(int, localDate)
и для получения типа LocalDate
с помощью getObject(int, LocalDate.class)
, если драйвер готов к этому. Не так элегантно, как более формальные методы default
, как вы и предполагали, конечно.
Ответ 2
Короткий ответ: нет, обработка времени даты фиксирована в Java 8/JDBC 4.2, поскольку она поддерживает Java 8 Типы даты и времени, Эту функцию нельзя использовать с использованием методов по умолчанию.
Java 8 может просто добавить эти методы по умолчанию в PreparedStatement и ResultSet:
Это невозможно по нескольким причинам:
-
java.time.OffsetDateTime
не имеет эквивалента в java.sql
-
java.time.LocalDateTime
ломается при переходах DST при преобразовании через java.sql.Timestamp
, потому что последний зависит от часового пояса 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());