Клиент MySQL и часовой пояс сервера

Я столкнулся с проблемой, когда MySQL хранит разные значения даты и времени, чем передает клиент. Сервер работает в формате UTC, а клиент — в другом часовом поясе. Каким-то образом MySQL, кажется, преобразует значения даты и времени между часовым поясом клиента и сервера, хотя типы SQL DATE, TIME и TIMESTAMP не имеют часового пояса. Ни одна другая база данных, которую я тестировал до сих пор, не имеет такого поведения.

Для воспроизведения проблемы можно использовать следующий код. Когда сервер работает в формате UTC, код работает только тогда, когда клиент также работает в формате UTC.

try (Connection connection = this.dataSource.getConnection();
     PreparedStatement preparedStatement = connection.prepareStatement(
             "SELECT ? = DATE '1988-12-25', ? = TIME '15:09:02', ? = TIMESTAMP '1980-01-01 23:03:20'")) {
  preparedStatement.setDate(1, java.sql.Date.valueOf("1988-12-25"));
  preparedStatement.setTime(2, java.sql.Time.valueOf("15:09:02"));
  preparedStatement.setTimestamp(3, java.sql.Timestamp.valueOf("1980-01-01 23:03:20"));
  try (ResultSet resultSet = preparedStatement.executeQuery()) {
    while (resultSet.next()) {
      System.out.println(resultSet.getBoolean(1));
      System.out.println(resultSet.getBoolean(2));
      System.out.println(resultSet.getBoolean(3));
    }
  }

}

я использую

  • MySQL 5.7.14
  • mysql-коннектор-java 6.0.5
  • Oracle Java 1.8.0_131

Мой URL-адрес JDBC просто jdbc:mysql://host:port/database

изменить

Мои рассуждения о том, почему часовые пояса не должны играть здесь роли и не должно происходить преобразование часовых поясов, двояки. Во-первых, на уровне SQL TIMESTAMP является псевдонимом для TIMESTAMP WITHOUT TIME ZONE, который строго подразумевает, что в отличие от TIMESTAMP WITH TIME ZONE его значения не имеют часового пояса. Другими словами, значения представляют собой не моменты времени, а скорее локальные значения даты и времени.

Во-вторых, то, что java.sql.Timestamp находится в часовом поясе JVM, является просто артефактом того, что он является подклассом java.util.Date. (Я знаю, что java.util.Date не имеет часового пояса). В Javadoc из java.sql.Timestamp из совершенно ясно, что отношения предназначены только для целей реализации.

Я чувствую, что оба эти утверждения подтверждаются тем фактом, что в Java SE 8/JDBC 4.2 java.sql.Timestamp отображается на java.time.LocalDateTime, а не на java.time.ZonedDateTime или java.time.OffsetDateTime.

изменить 2

Я не понимаю, почему значения TIMESTAMP подлежат преобразованию часового пояса. В отличие от TIMESTAMP WITH TIME ZOONE, это "локальные" значения, не связанные с часовым поясом, и поэтому к ним не должно применяться преобразование часовых поясов.


person Philippe Marschall    schedule 22.04.2017    source источник
comment
MySQL Connector/J (драйвер JDBC), обрабатывающий разницу часовых поясов (сервер UTC, клиент не UTC), кажется причудливой мешаниной непредсказуемого поведения. Существует несколько взаимосвязанных свойств конфигурации, которые влияют на поведение драйвера JDBC, обрабатывающего преобразования часового пояса для даты и времени и отметки времени, например. useLegacyDatetimeCode, useJDBCCompliantTimezoneShift, noTimezoneConversionForDateType,treatUtilDateAsTimestamp и др. Нам пришлось поэкспериментировать, чтобы найти комбинацию свойств соединения, которая привела бы к поведению, которое мы могли бы предсказать и приспособить.   -  person spencer7593    schedule 22.04.2017
comment
@ spencer7593 Я не могу найти ничего в текущая документация большинство из них, похоже, были удален в версии 6.0   -  person Philippe Marschall    schedule 22.04.2017
comment
Возможно, это лучше опубликовать в StackStatusReport, поскольку здесь, похоже, не задают никаких вопросов.   -  person spencer7593    schedule 22.04.2017
comment
Вопрос в том, почему этот код не работает и как мне заставить его работать.   -  person Philippe Marschall    schedule 22.04.2017
comment
Вы можете попробовать перегрузка, которая принимает Calendar для указания часового пояса.   -  person Mick Mnemonic    schedule 22.04.2017
comment
Сообщаемое поведение согласуется с поведением, которое мы ожидаем. В тексте оператора SQL есть литерал и значение привязки. Значение привязки подлежит преобразованию часового пояса (обработке даты и времени) в Connector/J. Литерал проходит как часть статического текста SQL без изменений. Если нам нужно сравнить два значения, чтобы оценить их как равные, нам нужно отключить преобразование часового пояса в значении привязки. Если мы установим часовой пояс клиента таким же, как часовой пояс сервера, преобразование часового пояса не произойдет (как уже было замечено). В противном случае нам нужно было бы отключить преобразование часового пояса.   -  person spencer7593    schedule 25.04.2017
comment
... (продолжение) или как-то обойти это. Чтобы код возвращал (нечетный) результат, мы могли бы передать значение привязки как другой тип данных (например, строку), чтобы оно не подвергалось преобразованию TZ. Или мы можем повлиять на поведение Connector/J в отношении преобразования часового пояса для типов данных Date, Datetime, Timestamp. Вот тут-то и появляется комментарий о свойствах Connector/J... влияющий на поведение. В новой версии 6.0 кажется, что MySQL, возможно, отказался от сбивающего с толку странного поведения в пользу того, чтобы просто делать правильные вещи. Или, может быть, просто недокументированные свойства.   -  person spencer7593    schedule 25.04.2017


Ответы (2)


Я согласен с комментарием о том, что обработка часовых поясов в MySQL JDBC может сбивать с толку, но в этом случае...

MySQL хранит другие значения даты и времени, чем клиент передает

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

Сначала вам нужно понять, что java.sql.Timestamp#valueOf создает значение Timestamp в местном часовом поясе, в котором работает виртуальная машина Java. Итак, для моей машины, которая находится в «горном времени» в Канаде (UTC-7 для стандартного времени):

System.out.printf("Local (client) timezone is %s%n", TimeZone.getDefault().getID());

java.sql.Timestamp tStamp = java.sql.Timestamp.valueOf("1980-01-01 23:03:20");

SimpleDateFormat sdfLocal = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
SimpleDateFormat sdfUTC = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
sdfUTC.setCalendar(Calendar.getInstance(TimeZone.getTimeZone("UTC")));

System.out.println("tStamp = java.sql.Timestamp.valueOf(\"1980-01-01 23:03:20\")");
System.out.printf("                        ... which is %s%n", sdfLocal.format(tStamp));
System.out.printf("                        ... which is %s%n", sdfUTC.format(tStamp));

отпечатки

Local (client) timezone is America/Edmonton
tStamp = java.sql.Timestamp.valueOf("1980-01-01 23:03:20")
                        ... which is 1980-01-01 23:03:20 MST
                        ... which is 1980-01-02 06:03:20 UTC

Теперь, когда мы передаем это значение Timestamp в PreparedStatement и отправляем его на сервер MySQL, который использует часовой пояс UTC, сервер интерпретирует и отображает строковые литеральные представления этого MySQL TIMESTAMP как UTC:

String connUrl = "jdbc:mysql://localhost/mydb";
try (Connection conn = DriverManager.getConnection(connUrl, myUid, myPwd)) {
    String sql = "SELECT CAST((TIMESTAMP ?) AS CHAR) AS foo";
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setTimestamp(1, tStamp);
        try (ResultSet rs = ps.executeQuery()) {
            rs.next();
            System.out.printf("String representation of TIMESTAMP value at server: %s%n", 
                    rs.getString(1));
        }
    }
}

производство

String representation of TIMESTAMP value at server: 1980-01-02 06:03:20

Если бы сервер MySQL работал по времени Торонто (UTC-5 для стандартного времени), вывод был бы...

String representation of TIMESTAMP value at server: 1980-01-02 01:03:20

... не потому, что значение MySQL TIMESTAMP отличается, а потому, что строковое представление этого значения в контексте часового пояса сервера MySQL отличается.

person Gord Thompson    schedule 23.04.2017
comment
Проблема, которую наблюдает OP, заключается в том, что код передает как значение literal (как часть текста SQL), так и значение bind. Значение привязки подвергается преобразованию часового пояса (обработке даты и времени) в Connector/J, в то время как литерал просто проходит (не затрагивается) как часть текста SQL. Сервер интерпретирует литерал в контексте часового пояса сервера. Поведение, наблюдаемое OP, ожидается. (Вероятно, OP позже столкнется с неожиданным поведением при преобразовании часового пояса, и именно тогда все эти запутанные свойства Connector/J станут важными.) +10. - person spencer7593; 25.04.2017

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

Я также кое-что узнал, читая ваши правки, например, java.sql.Timestamp не содержит часового пояса.

Как заявил spencer7593: «Сервер интерпретирует литерал в контексте часового пояса сервера».

Моими двумя подходами здесь будут:

  1. Подключитесь к серверу (UTC), указав свой часовой пояс в строке подключения (например, &serverTimezone=Europe/Zurich).

  2. В качестве альтернативы передайте экземпляр календаря с вашим локальным часовым поясом (Calendar.getInstance()) в PreparedStatement.setTimestamp.

person JackLeEmmerdeur    schedule 29.06.2017