Как получить временные метки UTC из JDBC + postgreSql timestamp?

В PostgreSQL я создал таблицу, подобную этой:

create table myTable (
    dateAdded timestamp(0) without time zone null default (current_timestamp at time zone 'UTC');
)

Я выбираю "без часового пояса", потому что знаю, что все временные метки, с которыми работает мое приложение, всегда являются UTC. Насколько я получил документацию, единственное отличие от "timestamp" заключается в том, что я могу предоставлять значения в других часовых поясах, которые затем будут преобразованы в UTC. Однако я хочу избежать таких автоматических преобразований, потому что они вряд ли могут принести пользу, если я знаю, что мои значения - это UTC.

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

Однако, когда я пытаюсь выбрать значения, используя JDBC, значение получает 2 часа. Я нахожусь в UTC + 2, так что похоже, что JDBC предполагает, что дата в таблице не является меткой времени UTC, а UTC + 2 timestamp вместо этого и пытается конвертировать в UTC.

Некоторые поисковые запросы показали, что стандарт JDBC диктует что-то о преобразовании в/из текущего часового пояса, но это можно предотвратить, предоставив Calander для вызова getTimestamp/setTimestamp. Однако поставка календаря не имела никакого значения. Вот мой конвертер MyBatis/Jodatime:

@MappedTypes(DateTime.class)
public class DateTimeTypeHandler extends BaseTypeHandler<DateTime> {
    private static final Calendar UTC_CALENDAR = Calendar.getInstance(DateTimeZone.UTC.toTimeZone());

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
            DateTime parameter, JdbcType jdbcType) throws SQLException {
        ps.setTimestamp(i, new Timestamp(parameter.getMillis()), UTC_CALENDAR);
    }

    @Override
    public DateTime getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        return fromSQLTimestamp(rs.getTimestamp(columnName, UTC_CALENDAR));
    }
    /* further get methods with pretty much same content as above */

    private static DateTime fromSQLTimestamp(final Timestamp ts) {
        if (ts == null) {
            return null;
        }
        return new DateTime(ts.getTime(), DateTimeZone.UTC);
    }
}

Какой правильный способ получить временные метки UTC из источника времени JDBC + PostgreSQL?

Ответ 1

Решение

Установите UTC в качестве часового пояса по умолчанию для вашей JVM -Duser.timezone=UTC или установите для всей ОС значение UTC.

Фон

В Postgres и TIMESTAMP, и TIMESTAMP WITH TIMEZONE хранятся одинаково - количество секунд, прошедшее с эпохи Postgres (2000-01-01). Основное отличие состоит в том, что делает Postgres, когда сохраняет значение метки времени, например 2004-10-19 10:23:54+02:

  • без TZ +02 просто убирается
  • с TZ выполняется коррекция -02, чтобы сделать ее UTC

Теперь интересно, когда драйвер JDBC загружает значение:

  • без TZ сохраненное значение смещается пользователем (JVM/OS) TZ
  • с TZ значение считается UTC

В обоих случаях вы получите объект java.sql.Timestamp с пользовательским TZ по умолчанию.

Часовые пояса

Метки времени без TZ довольно ограничены. Если к вашей базе данных подключены две системы с разными значениями TZ, они будут интерпретировать временные метки по-разному. Поэтому я настоятельно советую вам использовать TIMESTAMP WITH TIMEZONE.


Обновление

Вы можете сообщить JDBC, какой тип TZ следует использовать при чтении метки времени через ResultSet#getTimestamp(String, Calendar). Выдержка из JavaDoc:

Этот метод использует данный календарь для создания соответствующего значения в миллисекундах для метки времени, если базовая база данных не хранит информацию о часовом поясе.

Ответ 2

Решение, предложенное Pavel (добавление jvm param '-Duser.timezone = UTC'), наверняка является лучшим вариантом, если у вас нет доступа к системе, это не всегда можно сделать.

Единственный способ, которым я нашел, - преобразовать метку времени в epoch в запросе и прочитать ее как long.

SELECT  extract(epoch from my_timestamp_colum at time zone 'utc')::bigint * 1000 
            AS my_timestamp_as_epoc
FROM    my_table

Затем прочитайте его в простом JDBC с помощью

ResultSet rs = ...
long myTimestampAsEpoc = rs.getLong("my_timestamp_as_epoc");
Date myTimestamp = new Date(myTimestampAsEpoc);

С iBatis/MyBatis вам нужно обработать столбец как Long, а затем преобразовать его вручную в Date/Timestamp. Неудобный и уродливый.

Обратная операция в PostgreSQL может быть выполнена с помощью

SELECT TIMESTAMP WITHOUT TIME ZONE 'epoch' + 
       1421855729000 * INTERVAL '1 millisecond'

Тем не менее, спецификация JDBC не говорит, что возвращенная временная метка должна или не должна сдвигать метку времени в пользовательский часовой пояс; НО, так как вы определили столбец таблицы как "метка времени без часового пояса", я бы не ожидал сдвигов во времени, но записанная эпоха была просто завернута в java.sql.Timestamp. По моему мнению, это ошибка в драйвере PostgreSQL JDBC. Будучи проблемой на этом уровне, возможно, не так много, что можно сделать без доступа к системе.

Ответ 3

Есть несколько приемов, специфичных для драйвера Postgres JDBC

Смотрите https://jdbc.postgresql.org/documentation/head/java8-date-time.html

Таким образом, при чтении вы можете сделать

    Instant utc =resultSet.getObject("dateAdded",LocalDateTime.class).toInstant(ZoneOffset.UTC);

Если вы используете пул соединений, такой как Hikari, вы также можете указать часовой пояс, используемый каждым соединением, установив connectionInitSql =установить часовой пояс "UTC"