Что такое настройки драйвера JDBC-mysql для правильной обработки DATETIME и TIMESTAMP в UTC?

Будучи сгоревшим по часовой стрелке mysql и летним сбережением "час от ада" в прошлом, я решил, что мое следующее приложение будет хранить все в часовом поясе UTC и взаимодействовать только с базой данных с использованием UTC (даже не тесно связанных GMT).

Вскоре я столкнулся с некоторыми загадочными ошибками. Потянув мои волосы на некоторое время, я придумал этот тестовый код:

try(Connection conn = dao.getDataSource().getConnection();
    Statement stmt = conn.createStatement()) {

    Instant now = Instant.now();

    stmt.execute("set time_zone = '+00:00'");

    stmt.execute("create temporary table some_times("
            + " dt datetime,"
            + " ts timestamp,"
            + " dt_string datetime,"
            + " ts_string timestamp,"
            + " dt_epoch datetime,"
            + " ts_epoch timestamp,"
            + " dt_auto datetime default current_timestamp(),"
            + " ts_auto timestamp default current_timestamp(),"
            + " dtc char(19) generated always as (cast(dt as character)),"
            + " tsc char(19) generated always as (cast(ts as character)),"
            + " dt_autoc char(19) generated always as (cast(dt_auto as character)),"
            + " ts_autoc char(19) generated always as (cast(ts_auto as character))"
            + ")");

    PreparedStatement ps = conn.prepareStatement("insert into some_times "
            + "(dt, ts, dt_string, ts_string, dt_epoch, ts_epoch) values (?,?,?,?,from_unixtime(?),from_unixtime(?))");

    DateTimeFormatter dbFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("UTC"));

    ps.setTimestamp(1, new Timestamp(now.toEpochMilli()));
    ps.setTimestamp(2, new Timestamp(now.toEpochMilli()));
    ps.setString(3, dbFormat.format(now));
    ps.setString(4, dbFormat.format(now));
    ps.setLong(5, now.getEpochSecond());
    ps.setLong(6,  now.getEpochSecond());
    ps.executeUpdate();             

    ResultSet rs = stmt.executeQuery("select * from some_times");
    ResultSetMetaData md = rs.getMetaData();

    while(rs.next()) {
        for(int c=1; c <= md.getColumnCount(); ++c) {
            Instant inst1 = Instant.ofEpochMilli(rs.getTimestamp(c).getTime());
            Instant inst2 = Instant.from(dbFormat.parse(rs.getString(c).replaceAll("\\.0$", "")));
            System.out.println(inst1.getEpochSecond() - now.getEpochSecond());
            System.out.println(inst2.getEpochSecond() - now.getEpochSecond());
        }
    }
}

Обратите внимание, что для часового пояса сеанса задано значение UTC, и все в Java-коде очень чувствительны к часовому поясу и вынуждены использовать UTC. Единственная вещь во всей этой среде, которая не является UTC, - это часовой пояс JVM по умолчанию.

Я ожидал, что вывод будет связкой 0, но вместо этого я получаю этот

0
-28800
0
-28800
28800
0
28800
0
28800
0
28800
0
28800
0
28800
0
0
-28800
0
-28800
28800
0
28800
0

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

Кажется, что драйвер JDBC выполняет несоответствующие преобразования часовых поясов. Для приложения, которое полностью взаимодействует в UTC, хотя оно работает на виртуальной машине, а не в UTC, есть ли способ полностью отключить преобразования TZ?

то есть. Можно ли выполнить этот тест для вывода всех нулевых строк?

ОБНОВЛЕНИЕ

Использование useLegacyDatetimeCode=false (cacheDefaultTimezone=false не имеет значения) изменяет вывод, но все же не является исправлением:

0
-28800
0
-28800
0
-28800
0
-28800
0
-28800
0
-28800
0
-28800
0
-28800
0
0
0
0
0
0
0
0

UPDATE2

Проверка консоли (после изменения теста для создания постоянной таблицы), я вижу, что все значения STORED правильно:

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 27148
Server version: 5.7.12-log MySQL Community Server (GPL)

Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> set time_zone = '-00:00';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM some_times \G
*************************** 1. row ***************************
       dt: 2016-11-18 15:39:51
       ts: 2016-11-18 15:39:51
dt_string: 2016-11-18 15:39:51
ts_string: 2016-11-18 15:39:51
 dt_epoch: 2016-11-18 15:39:51
 ts_epoch: 2016-11-18 15:39:51
  dt_auto: 2016-11-18 15:39:51
  ts_auto: 2016-11-18 15:39:51
      dtc: 2016-11-18 15:39:51
      tsc: 2016-11-18 15:39:51
 dt_autoc: 2016-11-18 15:39:51
 ts_autoc: 2016-11-18 15:39:51
1 row in set (0.00 sec)

mysql>

Ответ 1

Решение состоит в том, чтобы установить параметр соединения JDBC noDatetimeStringSync=true с useLegacyDatetimeCode=false. В качестве бонуса я также обнаружил, что sessionVariables=time_zone='-00:00' явно скрывает необходимость set time_zone в каждом новом соединении.

Существует некоторый "интеллектуальный" код преобразования часового пояса, который активируется глубоко внутри метода ResultSet.getString(), когда он обнаруживает, что столбец является столбцом TIMESTAMP.

Увы, этот интеллектуальный код имеет ошибку: TimeUtil.fastTimestampCreate(TimeZone tz, int year, int month, int day, int hour, int minute, int seconds, int secondsPart) возвращает TIMESTAMP неправильно помеченную в часовой пояс JVM по умолчанию, даже если параметр tz установлен на что-то еще:

final static Timestamp fastTimestampCreate(TimeZone tz, int year, int month, int day, int hour, int minute, int seconds, int secondsPart) {
    Calendar cal = (tz == null) ? new GregorianCalendar() : new GregorianCalendar(tz);
    cal.clear();

    // why-oh-why is this different than java.util.date, in the year part, but it still keeps the silly '0' for the start month????
    cal.set(year, month - 1, day, hour, minute, seconds);

    long tsAsMillis = cal.getTimeInMillis();

    Timestamp ts = new Timestamp(tsAsMillis);
    ts.setNanos(secondsPart);

    return ts;
}

Возврат ts будет абсолютно корректным, если только он не будет продолжен в цепочке вызовов, он будет преобразован обратно в строку с использованием метода bare toString(), который отображает ts как строку, представляющую, что такое часы отображается в часовом поясе JVM-default, вместо строкового представления времени в UTC. В ResultSetImpl.getStringInternal(int columnIndex, boolean checkDateTypes):

                case Types.TIMESTAMP:
                    Timestamp ts = getTimestampFromString(columnIndex, null, stringVal, this.getDefaultTimeZone(), false);

                    if (ts == null) {
                        this.wasNullFlag = true;

                        return null;
                    }

                    this.wasNullFlag = false;

                    return ts.toString();

Настройка noDatetimeStringSync=true отключает весь беспорядок разбора/разбора и просто возвращает строковое значение as-получено из базы данных.

Тестируемый выход:

0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0

Значение useLegacyDatetimeCode=false по-прежнему важно, поскольку оно изменяет поведение getDefaultTimeZone() для использования сервера базы данных TZ.

Во время преследования этого я также обнаружил, что документация для useJDBCCompliantTimezoneShift неверна, хотя она не имеет значения: документация говорит [Это часть устаревшего кода даты и времени, поэтому свойство имеет эффект только тогда, когда "useLegacyDatetimeCode = true." ], но это неправильно, см. ResultSetImpl.getNativeTimestampViaParseConversion(int, Calendar, TimeZone, boolean).