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

Получение поля UTC DATETIME из MySQL в Java, когда часовой пояс сервера не UTC

Я пытаюсь написать код для взаимодействия с независимой от сторонней разработки базой данных с использованием Java и MySQL. В этой базе данных есть поле, которое хранит метку времени в поле DATETIME в качестве даты UTC. Часовой пояс для сервера, на котором запущены как база данных, так и клиент, настроен на не-UTC-зону (Europe/London), поэтому по умолчанию отметка времени считывается неправильно, как если бы это было локальное время. Я пытаюсь написать код, чтобы прочитать его как UTC.

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

К сожалению, я не могу изменить настройки сервера, поэтому я попытался использовать переменную "time_zone" соединения, чтобы установить сервер базы данных на использование UTC и необязательный параметр Calendar на ResultSet.getTimestamp для получения даты, но это не влияет на результат. Вот мой код:

private static final Calendar UTCCALENDAR = Calendar.getInstance (TimeZone.getTimeZone (ZoneOffset.UTC));
public Date getDate ()
{
    try (Connection c = dataSource.getConnection ();
         PreparedStatement s = c
             .prepareStatement ("select datefield from dbmail_datefield where physmessage_id=?"))
    {
        fixTimeZone (c);
        s.setLong (1, getPhysId ());
        try (ResultSet rs = s.executeQuery ())
        {
            if (!rs.next ()) return null;
            return new Date (rs.getTimestamp(1,UTCCALENDAR).getTime ());    // do not use SQL timestamp object, as it fucks up comparisons!
        }
    }
    catch (SQLException e)
    {
        throw new MailAccessException ("Error accessing dbmail database", e);
    }
}

private void fixTimeZone (Connection c)
{
    try (Statement s = c.createStatement ())
    {
        s.executeUpdate ("set time_zone='+00:00'");
    }
    catch (SQLException e)
    {
        throw new MailAccessException ("Unable to set SQL connection time zone to UTC", e);
    }
}

В поле базы данных, которое я пытаюсь прочитать, есть значение, хранящееся в нем:

mysql> select * from dbmail_datefield where physmessage_id=494539;
+----------------+--------+---------------------+
| physmessage_id | id     | datefield           |
+----------------+--------+---------------------+
|         494539 | 494520 | 2015-04-16 10:30:30 |
+----------------+--------+---------------------+

Но, к сожалению, результат выдается как BST не UTC:

java.lang.AssertionError: expected:<Thu Apr 16 11:30:30 BST 2015> but was:<Thu Apr 16 10:30:30 BST 2015>
4b9b3361

Ответ 1

Код клиента getDate() выглядит корректно, насколько это возможно. Я думаю, вам также нужно, чтобы драйвер MySQL Connector/J JDBC обрабатывал даты, хранящиеся в таблице, в виде дат UTC, чтобы избежать ложного преобразования часового пояса. Это означает настройку эффективного часового пояса сервера в дополнение к часовому поясу сеанса клиента и Календалу, используемому для вызовов JDBC getTimestamp, как вы это делаете.

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

expected:<Thu Apr 16 11:30:30 BST 2015> but was:<Thu Apr 16 10:30:30 BST 2015>

То, что вы получили, было 10:30 BST, что составляет 9:30 GMT. Это согласуется с тем, что база данных обрабатывает 10:30 в таблице как значение BST и ложно преобразует ее в GMT для вас, прежде чем вы проанализируете ее как дату GMT. То, что противоположное направление значения GMT ложно преобразуется в BST.

Это может быть проблема JDBC, потому что JDBC требует, чтобы время было преобразовано в локальную зону. (Там, где API MySQL C не существует, возможно, потому, что классические типы времени C не являются зонирующими, как Java.) И он должен знать, из какой зоны он преобразуется. Тип MySQL TIMESTAMP всегда сохраняется как UTC. Но это не указано для типа DATETIME. Я думаю, это означает, что MySQL будет интерпретировать значения столбца DATETIME как находящиеся в часовом поясе сервера. Который вы упомянули как установленный в BST, и который соответствует направлению сдвига, указанному в сообщении об ошибке утверждения.

Установленная переменная сеанса time_zone сообщает серверу MySQL, что такое часовой пояс вашего клиентского компьютера, но это не влияет на то, что сервер считает своим собственным часовым поясом. Это можно переопределить с помощью свойства serverTimezone JDBC connection. В вашем соединении установите serverTimezone в UTC и убедитесь, что useLegacyDatetimeCode выключен. (И просмотрите другие свойства, связанные с зоной, если это не сработает.) Посмотрите, получит ли эти даты в качестве UTC с теми же значениями поля календаря, что и в базе данных.

Помните, что это изменит интерпретацию других значений DATETIME в вашей базе данных: теперь они будут выглядеть как даты UTC (в контексте вашего соединения JDBC). Правильно ли это будет зависеть от того, как они были заселены на начальном этапе. Хотя ваш код клиента будет иметь поведение, которое вы хотите, я не знаю, можно ли заставить эту систему в целом полностью вести себя, не устанавливая часовой пояс сервера на UTC на уровне сервера. В принципе, если у нее нет своей зоны, установленной в UTC, она не полностью настроена для поведения, которое вы хотите, и вы обманываете его.

Ответ 2

Возможно, вы можете использовать JodaTime следующим образом:

private static final Calendar UTCCALENDAR = Calendar.getInstance (TimeZone.getTimeZone (ZoneOffset    .UTC));
public Date getDate ()
{
    try (Connection c = dataSource.getConnection ();
         PreparedStatement s = c
             .prepareStatement ("select datefield from dbmail_datefield where physmessage_id=?"))
    {
        s.setLong (1, getPhysId ());
        try (ResultSet rs = s.executeQuery ())
        {
            if (!rs.next ()) return null;
            DateTime dt = new LocalDateTime(rs.getTimestamp(1,UTCCALENDAR).getTime ()).toDateTime(DateTimeZone.forID("Europe/London"));  

            return dt.toDate();               }
    }
    catch (SQLException e)
    {
        throw new MailAccessException ("Error accessing dbmail database", e);
    }
}

EDIT:

java.util.Date не является агностиком TimeZone. Метод toDateTime позаботится о TimeZone и DST, поэтому вам все равно,

Следующий код:

public static void main(String[] args) {
    // 29/March/2015 1:05 UTC
    DateTime now = new DateTime(2015, 3,29,1,5,DateTimeZone.UTC);
    // Pre DST 29/March/2015 0:30 UTC
    DateTime preDst = new DateTime(2015, 3,29,0,30,DateTimeZone.UTC);
    System.out.println("1:05 UTC:"+now);
    System.out.println("0:30 UTC:"+preDst);
    DateTimeZone europeDTZ = DateTimeZone.forID("Europe/London");
    DateTime europeLondon = now.toDateTime(europeDTZ);
    System.out.println("1:05 UTC as Europe/London:"+europeLondon);
    DateTime europeLondonPreDst = preDst.toDateTime(europeDTZ);
    System.out.println("0:30 UTC as Europe/London:"+europeLondonPreDst);
}

Будет напечатан:

1:05 UTC:2015-03-29T01:05:00.000Z
0:30 UTC:2015-03-29T00:30:00.000Z
1:05 UTC as Europe/London:2015-03-29T02:05:00.000+01:00
0:30 UTC as Europe/London:2015-03-29T00:30:00.000Z

Если вы видите, что JodaTime заботится о DST.

Ответ 3

Если вы хотите использовать часовой пояс, вы можете читать столбцы в формате UTC.

ZonedDateTime zdt = ZonedDateTime.of(rs.getTimestamp(1).toLocalDateTime(), ZoneOffset.UTC);

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

zdt = zdt.withZoneSameInstant(ZoneId.of(
            TARGET_ZONE));

Если вы хотите только прочитать дату и не заботиться о зонах, которые используются только:

LocalDateTime ldt = rs.getTimestamp(1).toLocalDateTime()

Вы получите LocalDateTime без часовой пояс.

Если вам нужно вернуть java.util.Date, используйте:

Date.from(ldt.atZone(ZoneOffset.UTC).toInstant());

Ответ 4

Лучше всего, на мой взгляд, сказать MySQL использовать GMT и обрабатывать все локальные проблемы времени в коде приложения, а не в вашей базе данных. Значения в базе данных всегда будут GMT, полная остановка, что недвусмысленно. Как вы говорите, с корректировками дневного времени (летнее время) вы можете получить то же значение в своей базе данных, что и для нас, людей в два разных раза.

Это также делает базу данных переносимой. Если вы перейдете в Северную Америку и начнете использовать MySQL, установленное (скажем) центральное время, внезапно значения в вашей базе данных, похоже, переместились на несколько часов. У меня была проблема с базой данных, которую я унаследовал, которая использовала локальное время сервера, когда я переместил ее с восточного побережья США на западное побережье, не подумав проверить, была ли MySQL подчинена машинной зоне...

long t = 1351382400000; // the timestamp in UTC
String insert = "INSERT INTO my_table (timestamp) VALUES (?)";
PreparedStatement stmt = db.prepareStatement(insert);
java.sql.Timestamp date = new Timestamp(t);
stmt.setTimestamp(1, date);
stmt.executeUpdate();

.....

TimeZone timezone = TimeZone.getTimeZone("MyTimeZoneId");
Calendar cal = java.util.Calendar.getInstance(timezone);
String select = "SELECT timestamp FROM my_table";
// some code omitted....
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
   java.sql.Timestamp ts = rs.getTimestamp(1);
   cal.setTimeInMillis(ts.getTime());
   System.out.println("date in db: " + cal.getTime());
}

Ответ 5

Не думайте о преобразовании или адаптации часового пояса. Не думайте о TZ, который использует mysql для хранения ваших временных меток или чего-то подобного. Те вещи уже обрабатываются. Есть три вещи, которые вы должны обрабатывать: INPUT, OUTPUT и ошибки.

ВХОД

Когда пользователь вводит дату (в форме) без явного часового пояса, вы должны знать, что TZ он намеревался использовать. Вы можете использовать объект SimpleDateFormat с часовым поясом, чтобы решить эту проблему. Вам не нужно преобразовывать дату ввода, вы должны правильно ее интерпретировать. После того, как вы правильно указали дату или временную метку, вы делаете это с помощью ввода.

Ввод - это не только ввод пользователя, но и файлы конфигурации.

OUTPUT

То же самое здесь. Забудьте о том, что у TZ ваши объекты Date и timestamps нет, они всего лишь миллисекунды с эпохи. Вы должны отформатировать свои даты в TZ, который пользователь ожидает, чтобы он их понял.

Ошибки

У вас могут быть ошибки в коде, связанные с TZ, но библиотеки могут иметь их тоже!

Я заметил, что драйвер mysql java не смог связать часовой пояс клиента с сервером. Эта команда s.executeUpdate ("set time_zone='+xx:yy'"); является обходным решением, но вы используете его неправильно. Вы должны указать серверу с ним, который использует клиент TZ, перед вставкой и запросом. Переменная сохраняется в сеансе. Возможно, вы можете автоматизировать его в конфигурации пула соединений. Это необходимо, чтобы сервер знал, что TZ клиент должен использовать для чтения или записи. Это не зависит от сервера TZ. Это не означает "сохранить эту дату в UTC", это означает "эта дата, которую я даю вам, это UTC" и "Отправить мне результирующие наборы в UTC". Независимо от того, используете ли вы класс Date с его внутренним TZ, драйвер закручивает его, вам нужно будет установить эту переменную сеанса.

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