Часовые пояса в SQL DATE vs java.sql.Date - PullRequest
36 голосов
/ 09 февраля 2012

Меня немного смущает поведение типа данных 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.Timestamp вместо java.sql.Date
  • Это аналогичный вопрос, который, однако, не отвечает на этот вопрос: java.util.Date vs java.sql.Date

Ответы [ 7 ]

38 голосов
/ 16 февраля 2012

Спецификация JDBC не определяет никаких деталей относительно часового пояса.Тем не менее, большинство из нас знает, что приходится сталкиваться с противоречиями часового пояса JDBC;просто посмотрите на все вопросы StackOverflow !

В конечном счете, обработка часового пояса для типов баз данных даты / времени сводится к серверу баз данных, драйверу JDBC и всему промежуточному.Вы даже во власти ошибок драйвера JDBC;PostgreSQL исправил ошибку в версии 8.3 , где

Методы Statement.getTime, .getDate и .getTimestamp, которые передаются объекту Calendar, вращали часовой пояс в неправильном направлении.

Когда вы создаете новую дату, используя new Date(0) (допустим, вы используете Oracle JavaSE java.sql.Date, ваша дата создается

с использованиемзаданное значение времени в миллисекундах. Если указанное значение в миллисекундах содержит информацию о времени, драйвер установит временные компоненты на время в часовом поясе по умолчанию (часовом поясе виртуальной машины Java, на которой выполняется приложение), что соответствует нулю по Гринвичу.

Итак, new Date(0) должен использовать GMT.

Когда вы вызываете ResultSet.getDate(int), вы выполняете реализацию JDBC. Спецификация JDBC не делаетдиктовать, как реализация JDBC должна обрабатывать детали часового пояса, поэтому вы находитесь в зависимости от реализации. Глядя на Oracle 11g oracle.sql.DATE JavaDoc, он неeem Oracle DB хранит информацию о часовом поясе, поэтому она выполняет свои собственные преобразования, чтобы получить дату в java.sql.Date.У меня нет опыта работы с Oracle DB, но я предполагаю, что реализация JDBC использует настройки часового пояса сервера и вашей локальной JVM для преобразования из oracle.sql.DATE в java.sql.Date.


Вы упомянуличто несколько реализаций СУБД правильно обрабатывают часовой пояс, за исключением SQLite.Давайте рассмотрим, как H2 и SQLite работают при отправке значений даты в драйвер JDBC и при получении значений даты из драйвера JDBC.

Драйвер H2 JDBCPrepStmt.setDate(int, Date) использует ValueDate.get(Date), который вызывает DateTimeUtils.dateValueFromDate(long), который выполняет преобразование часового пояса.

Использование этого SQLiteДрайвер JDBC , PrepStmt.setDate(int, Date) вызывает PrepStmt.setObject(int, Object) и не выполняет преобразование часового пояса.

Драйвер JDBC H2 JdbcResultSet.getDate(int) возвращает get(columnIndex).getDate().get(int) возвращает H2 Value для указанного столбца.Поскольку тип столбца DATE, H2 использует ValueDate.ValueDate.getDate() вызывает DateTimeUtils.convertDateValueToDate(long), что в конечном итоге создает java.sql.Date после преобразования часового пояса.

Использование этого драйвера SQLite JDBC код RS.getDate(int) намного проще;он просто возвращает java.sql.Date, используя значение даты long, хранящееся в базе данных.

Таким образом, мы видим, что драйвер JDBC H2 хорошо разбирается в обработке часовых поясов с датами, а драйвер SQLite JDBC - нет.(Нельзя сказать, что это решение не является разумным, оно может хорошо подойти к проектным решениям SQLite).Если вы будете искать источник для других драйверов RDBMS JDBC, о которых вы упомянули, вы, вероятно, обнаружите, что большинство приближается к дате и часовому поясу аналогично тому, как это делает H2.

Хотя спецификации JDBC не детализируют времяобработка зон, имеет смысл, что разработчики реализации RDBMS и JDBC приняли во внимание часовой пояс и будут обрабатывать его должным образом;особенно если они хотят, чтобы их продукция продавалась на мировой арене.Эти дизайнеры чертовски умны, и я не удивлен, что большинство из них понимают это правильно, даже при отсутствии конкретной спецификации.


Я нашел этот блог по Microsoft SQL Server, Используя времяданные о зонах в SQL Server 2008 , в которых объясняется, как часовой пояс усложняет ситуацию:

часовые пояса представляют собой сложную область, и каждому приложению необходимо будет указать, как вы собираетесь обрабатывать данные часового пояса, чтобы сделатьпрограммы более удобные для пользователя.

К сожалению, в настоящее время не существует международного стандарта для имен и значений часовых поясов. Каждая система должна использовать систему по своему выбору, и пока не существует международного стандарта, невозможно попытаться сделать так, чтобы SQL Server ее предоставил, и в конечном итоге это вызовет больше проблем, чем решит.

11 голосов
/ 16 февраля 2012

Преобразование выполняется драйвером 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. Следовательно, вы «потеряли» час по сравнению с первоначальной датой.

5 голосов
/ 21 февраля 2012

Оба объекта java.util.Date и Oracle / MySQL Date являются просто представлениями точки во времени независимо от местоположения.Это означает, что вполне вероятно, что оно будет храниться внутри системы как количество милли / нано секунд со времени «эпохи», GMT 1970-01-01 00: 00: 00.

Когда вы читаете из набора результатов,Ваш вызов «rs.getDate ()» говорит результирующему набору принять внутренние данные, содержащие момент времени, и преобразовать их в объект Date Java.Этот объект даты создается на вашем локальном компьютере, поэтому Java выберет ваш местный часовой пояс, который является CET для Цюриха.

Разница, которую вы видите, заключается в разнице в представлении, а не во времени.

4 голосов
/ 16 февраля 2012

Класс java.sql.Date соответствует SQL DATE, который не хранит информацию о времени или часовом поясе. Это достигается путём «нормализации» даты, как в javadoc :

Чтобы соответствовать определению SQL DATE, значения миллисекунд, обернутые экземпляром java.sql.Date, должны быть «нормализованы» путем установки часов, минут, секунд и миллисекунд на ноль в конкретном часовом поясе, с которым Экземпляр связан.

Это означает, что когда вы работаете в UTC + 1 и запрашиваете у DATE базу данных, совместимая реализация делает именно то, что вы наблюдали: возвращает java.sql.Date со значением в миллисекундах, которое соответствует рассматриваемой дате в 00:00:00 UTC + 1 независимо от того, как данные попали в базу данных.

Драйверы базы данных могут позволить изменить это поведение с помощью параметров, если это не то, что вам нужно.

С другой стороны, когда вы передаете java.sql.Date в базу данных, драйвер будет использовать часовой пояс по умолчанию, чтобы отделить компоненты даты и времени от значения в миллисекундах. Если вы используете 0 и находитесь в UTC + X, дата будет 1970-01-01 для X> = 0 и 1969-12-31 для X <0. </p>


Sidenote: Странно видеть, что документация для Date(long) конструктора отличается от реализации. Javadoc говорит это:

Если указанное значение в миллисекундах содержит информацию о времени, драйвер установит временные компоненты на время в часовом поясе по умолчанию (часовой пояс виртуальной машины Java, на которой запущено приложение), что соответствует нулевому времени по Гринвичу.

Однако, что на самом деле реализовано в 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?

2 голосов
/ 16 февраля 2012

Существует перегрузка 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 возвращает текущую локаль, в противном случае вам придется поискать, как выполнить быстрое преобразование часового пояса.

1 голос
/ 27 мая 2019

JDBC 4.2 и java.time

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.time, современного Java-API даты и времени.В JDBC 4.2 указано, что вы можете передавать объекты java.time, включая LocalDate, в вашу базу данных SQL и из нее с помощью методов setObject и getObject, которые я использую во фрагменте (поэтому не setDate и getDate).

Я думаю, что практически все мы сейчас используем драйверы JDBC 4.2.

Что пошло не так в вашем коде?

java.sql.Date - попытка взлома (не очень успешно)замаскировать java.util.Date как дату без времени суток.Я рекомендую не использовать этот класс.

Из документации:

Чтобы соответствовать определению SQL DATE, значения в миллисекундах заключаются в java.sql.DateЭкземпляр должен быть «нормализован» путем установки часов, минут, секунд и миллисекунд на ноль в конкретном часовом поясе, с которым связан экземпляр.

«Связанный» может быть расплывчатым.Я полагаю, что это означает, что значение в миллисекундах должно обозначать начало дня в часовом поясе JVM по умолчанию (в вашем случае Европа / Цюрих, я полагаю).Поэтому, когда вы создаете new java.sql.Date(0), равный GMT 1970-01-01 00:00:00, это действительно вы создаете неправильный объект Date, так как это время 01:00:00 в вашем часовом поясе.JDBC игнорирует время суток (возможно, мы ожидали сообщения об ошибке, но не получили его).Так что SQL получает и возвращает дату 1970-01-01.JDBC правильно переводит это обратно в Date, содержащий CET 1970-01-01 00:00:00.Или значение в миллисекундах -3 600 000. Да, это сбивает с толку.Как я уже сказал, не используйте этот класс.

Если бы вы имели отрицательное смещение по Гринвичу (например, большинство часовых поясов в Северной и Южной Америке), new java.sql.Date(0) было бы интерпретировано как 1969-12-31, так что это была бы дата, которую вы передали и вернули из SQL.

Что еще хуже, подумайте, что происходит, когда изменяется настройка часового пояса JVM.Любая часть вашей программы и любая другая программа, работающая в той же JVM, может делать это в любое время.Обычно это приводит к тому, что все java.sql.Date объекты, созданные до изменения, становятся недействительными и могут иногда сдвигать дату на один день (в редких случаях на два дня).

Ссылки

0 голосов
/ 05 апреля 2019

Я думаю, что проблемы с часовыми поясами слишком сложны. Когда вы передаете дату / время подготовленной выписке, она только отправляет информацию о дате и / или времени на сервер базы данных. Согласно стандарту defacto, он не управляет разницей часовых поясов между сервером базы данных и jvm. И позже, когда вы читаете дату / время из набора результатов. Он просто читает информацию о дате и времени из базы данных и создает ту же дату / время в часовом поясе jvm.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...