Подтвердить что ты не робот

Часовой пояс в SQL DATE vs java.sql.Date

Я немного смущен поведением типа данных 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
4b9b3361

Ответ 1

Спецификация 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.sql.DATE Oracle 11g oracle.sql.DATE, кажется, что Oracle DB не хранит информацию о часовом поясе, поэтому выполняет свои собственные преобразования для получения даты в java.sql.Date. У меня нет опыта работы с Oracle DB, но я предполагаю, что реализация JDBC использует сервер и ваши локальные настройки часового пояса JVM для преобразования из oracle.sql.DATE в java.sql.Date.


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

Драйвер JDBC H2 PrepStmt.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 хорошо умеет обрабатывать преобразования часовых поясов с датами, а драйвер JDBC SQLite - нет (не говоря уже о том, что это решение не является разумным, оно может хорошо подойти к проектным решениям 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. Следовательно, вы "потеряли" час по сравнению с исходной датой.

Ответ 3

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

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

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

Ответ 4

Класс 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.


Sidenote: Нечетно видеть, что документация для конструктора Date(long) отличается от реализации. Джавадок говорит следующее:

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

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

Ответ 5

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

Ответ 6

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

Ответ 7

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 JVMs time zone setting
// since a LocalDate doesnt 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 и из LocalDate через методы 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 -3 1, так что это была бы дата, когда вы перешел и вернулся из SQL.

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

связи