Часовой пояс в 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
Ответы
Ответ 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
созданные до изменения, становятся недействительными и могут иногда сдвигать дату на один день (в редких случаях на два дня).
связи