Временная метка Java и Oracle с часовым поясом

#oracle #hibernate #timestamp #ojdbc #offsetdatetime

#Oracle #спящий режим #временная метка #ojdbc #offsetdatetime

Вопрос:

Я пытаюсь сохранить две даты в Oracle с помощью гибернации. обе даты имеют одинаковые временные метки в московском часовом поясе: 2005-10-30T02:00 03:00 [Европа / Москва] и 2005-10-30T02:00 04:00 [Европа / Москва] («Вс Окт. 30 02:00:00 MSK 2005» и «Вс окт. 30 02:00:00 MSD 2005»). даты разделены по времени на один час и связаны с переходом на зимнее / летнее время.

Я создал таблицу в Oracle:

 create table TMP
(
    ID    LONG,
    TS    TIMESTAMP,
    TSLTZ TIMESTAMP WITH LOCAL TIME ZONE,
    TSTZ  TIMESTAMP WITH TIME ZONE
);
 

и объект в моем модуле:

 @Entity
@Table(name = "tmp")
public class DateTimeOracle {

    private Long id;
    private ZonedDateTime ts;
    private ZonedDateTime tsltz;
    private ZonedDateTime tstz;

    @Id
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public ZonedDateTime getTs() {
        return ts;
    }

    public ZonedDateTime setTs(ZonedDateTime ts) {
        this.ts = ts;
    }

    public ZonedDateTime getTsltz() {
        return tsltz;
    }

    public ZonedDateTime setTsltz(ZonedDateTime tsltz) {
        this.tsltz = tsltz;
    }

    public ZonedDateTime getTstz() {
        return tstz;
    }

    public ZonedDateTime setTstz1(ZonedDateTime tstz) {
        this.tstz = tstz;
    }
}
 

В entity все поля инициализируются одной датой.
После сохранения обе даты в Oracle имеют одинаковые значения и выглядят как:

ts = 2005-10-30 02:00:00.000000

TSLTZ = 2005-10-29 23:00:00.000000

TSTZ = 2005-10-30 02:00:00.000000 04:00

Почему oracle сохраняет одно и то же значение для разных дат (включая смещение 04:00)? Есть ли способ это исправить?

P.S. Postgres сохраняет дату правильно. Один со смещением 03:00, другой со смещением 04:00 (2005-10-29 23:00:00.000000 и 2005-10-29 22:00:00.000000, соответственно).

Обновить

Вот как я создаю даты:

 Date dt2 = new Date(1130623200000L); //2005-10-29 23:00:00  04:00
Date dt3 = new Date(1130626800000L); //2005-10-29 23:00:00  03:00
ZonedDateTime zdt2 = ZonedDateTime.ofInstant(dt2.toInstant(), ZoneId.systemDefault()); // My zone is MSK
ZonedDateTime zdt3 = ZonedDateTime.ofInstant(dt3.toInstant(), ZoneId.systemDefault()); // My zone is MSK
OffsetDateTime odt2 = zdt2.toOffsetDateTime();
OffsetDateTime odt3 = zdt3.toOffsetDateTime();
 

Если я не использую спящий режим и использую jdbc напрямую, ситуация не изменится.

 Connection conn = DriverManager.getConnection("<oracle_url>",
                        "<username>", "<password>");
PreparedStatement pstmt = conn.prepareStatement("insert into tmp (id, TSTZ1, TSTZ2) values (200, ?, ?)", Statement.RETURN_GENERATED_KEYS);
pstmt.setDate(1, new java.sql.Date(dt2.getTime()));
pstmt.setDate(2, new java.sql.Date(dt3.getTime()));
int z1 = pstmt.executeUpdate();
pstmt.close();
conn.close();
 

Обновление 2

Если я сохраню OffsetDateTime через драйвер jdbc в метке времени с локальным часовым поясом или в метке времени с часовым поясом, тогда все в порядке.

 PreparedStatement pstmt = conn.prepareStatement("insert into tmp (TSLTZ1, TSLTZ2, TSTZ1, TSTZ2) values (?, ?, ?, ?)");    
pstmt.setObject(1, odt2);
pstmt.setObject(2, odt3);
pstmt.setObject(3, odt2);
pstmt.setObject(4, odt3);
 

Я вижу в БД:

 2005-10-29 22:00:00.000000  2005-10-29 23:00:00.000000  2005-10-30 02:00:00.000000  04:00   2005-10-30 02:00:00.000000  03:00
 

Но если я сохраню ZonedDateTime, то значения в timestamp с локальным часовым поясом будут правильными, но в timestamp с часовым поясом будут неверными.

 PreparedStatement pstmt = conn.prepareStatement("insert into tmp (TSLTZ1, TSLTZ2, TSTZ1, TSTZ2) values (?, ?, ?, ?)");    
pstmt.setObject(1, zdt2);
pstmt.setObject(2, zdt3);
pstmt.setObject(3, zdt2);
pstmt.setObject(4, zdt3);
 

В БД я вижу:

 2005-10-29 22:00:00.000000  2005-10-29 23:00:00.000000  2005-10-30 02:00:00.000000  04:00   2005-10-30 02:00:00.000000  04:00
 

Последние два значения неверны.

Комментарии:

1. Почему вы используете тип данных LONG ? Он устарел целую вечность.

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

3. Я не думаю java.sql.Date , что это правильный класс. java.sql.Timestamp может быть лучше.

4. Ваш вопрос действительно сбивает с толку. Пожалуйста, сделайте четкое заявление для одной записи : что вы вставили? Каков результат (с использованием правильного выходного формата)? И чего вы ожидаете вместо этого? Затем, если необходимо, сделайте то же самое для второго значения.

5. But I don't understand why this works wrong: pstmt.setDate(2, new java.sql.Date(dt2.getTime())); — Вы не должны тратить свое время на анализ этого. API даты и времени java.util и их API форматирования SimpleDateFormat устарели и подвержены ошибкам. Рекомендуется полностью прекратить их использование и переключиться на современный API даты и времени . Вы уже упоминали: If I save the date as pstmt.setObject(1, odt2); then all is ok! . Обратите внимание, что java.util.Date в нем нет информации о часовом поясе.

Ответ №1:

Некоторые пояснения о типах данных временных меток Oracle:

  • TIMESTAMP : Не сохраняет никакой информации о часовом поясе. Если вы вводите временную метку с указанием часового пояса, то информация о часовом поясе просто усекается и теряется.
  • TIMESTAMP WITH TIME ZONE : Сохраняет временную метку с информацией о часовом поясе (т.Е. Либо Как именованный регион, либо как смещение UTC) при вставке временной метки в базу данных.
  • TIMESTAMP WITH LOCAL TIME ZONE : Временная метка сохраняется как DBTIMEZONE (рекомендуется и обычно UTC ). Временная метка отображается всегда и только в текущем сеансе пользователя SESSIONTIMEZONE . Таким образом, он не отображает никакой информации о часовом поясе, потому что по определению это всегда ваш местный часовой пояс.

Какой из них я должен использовать?

Конечно, это зависит от ваших требований.

Поскольку TIMESTAMP WITH LOCAL TIME ZONE вам не нужно заботиться о каких-либо настройках вашего клиента, время всегда отображается по местному времени. Время сохраняется в DBTIMEZONE памяти, таким образом, вы теряете исходный вставленный часовой пояс.

Имейте в виду, когда вы создаете индекс на TIMESTAMP WITH TIME ZONE . Невозможно создать индекс непосредственно по такому столбцу. Вместо этого Oracle создает виртуальный столбец для SYS_EXTRACT_UTC(TSTZ) и создает индекс для этого виртуального столбца. Вам следует обратить на это внимание при разработке своих запросов.

Обновить

У вас совершенно особая ситуация. Когда вы вставляете TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow' , то это время неоднозначно, это может означать 2005-10-30 02:00:00 03:00 или 2005-10-30 02:00:00 04:00

Возьмем этот пример:

 SELECT TO_CHAR(TIMESTAMP '2005-10-30 00:00:00 Europe/Moscow'   LEVEL * INTERVAL '1' HOUR, 
    'YYYY-MM-DD hh24:mi:ss TZH:TZM TZD tzr') AS ts
FROM dual
CONNECT BY LEVEL <= 4;

 -------------------------------------------- 
|TS                                          |
 -------------------------------------------- 
|2005-10-30 01:00:00  04:00 MSD Europe/Moscow|
|2005-10-30 02:00:00  04:00 MSD Europe/Moscow|
|2005-10-30 02:00:00  03:00 MSK Europe/Moscow|
|2005-10-30 03:00:00  03:00 MSK Europe/Moscow|
 -------------------------------------------- 
 

Взгляните на ВРЕМЕННУЮ МЕТКУ С типом данных ЧАСОВОГО ПОЯСА

Чтобы устранить неоднозначность граничных случаев, когда время переключается со стандартного времени на летнее, используйте как элемент формата TZR, так и соответствующий элемент формата TZD. Элемент формата TZD представляет собой аббревиатуру региона часового пояса с включенной информацией о переходе на летнее время. Примерами могут служить PST для тихоокеанского стандартного времени США и PDT для тихоокеанского дневного времени США. Следующая спецификация гарантирует, что будет возвращено значение летнего времени:

 TIMESTAMP '1999-10-29 01:30:00 America/Los_Angeles PDT'
 

Если вы не добавляете элемент формата TZD, а значение datetime неоднозначно, база данных Oracle возвращает ошибку, если ERROR_ON_OVERLAP_TIME для параметра сеанса установлено значение TRUE . Если ERROR_ON_OVERLAP_TIME установлено значение FALSE (значение по умолчанию), то база данных Oracle интерпретирует неоднозначную дату и время как стандартное время.

Обратите внимание, часовой пояс 04:00 или 03:00 не равен Europe/Moscow . Часовой Europe/Moscow пояс учитывает переход на летнее время (когда он все еще использовался в России около 10 лет назад), но 04:00 / 03:00 этого не делает.

Извините, я никогда не использовал hibernate, поэтому я не знаю, как этот фреймворк обрабатывает такие данные. Я тоже не знаком с Java. Возможно, информация о переходе на летнее время не поддерживается.

Я могу только догадываться, класс java.sql.Date и метод setDate относятся к DATE типу данных в Oracle. Как уже говорилось, лучше использовать java.sql.Timestamp и setTimestamp .

Тип DATE данных не поддерживает никакой информации о часовом поясе. Если вы попытаетесь вставить DATE значение в TIMESTAMP WITH [LOCAL] TIME ZONE столбец, то Oracle действительно это сделает

 FROM_TZ(CAST(<your DATE value> AS TIMESTAMP), SESSIONTIMEZONE)
 

Комментарии:

1. Спасибо за информацию, но вопрос был другим. Почему Oracle сохраняет одно и то же значение для двух разных дат?

2. Что такое «две разные даты» и результат? Я совершенно уверен, что когда вы вставляете 2005-10-30T02:00 03:00 , то TSTZ, конечно, нет 2005-10-30 02:00:00.000000 04:00

3. Я подумал то же самое, но когда я вставляю 2005-10-30T02:00 03:00, то TSTZ я вижу в базе данных 2005-10-30 02:00:00.000000 04:00. Вы можете попробовать сохранить указанные даты в базе данных самостоятельно.

4. Редактировать: я сохраняю не 2005-10-30T02:00 03:00, а zdt3 и odt3 (см. Раздел вверху)

5. Вы правы, но когда я сохраняю даты из Java, у меня в базе данных неправильные значения, даже если я использую jdbc напрямую без спящего режима.

Ответ №2:

Ваши условия очень особенные, давайте попробуем немного облегчить их.

Время Unix 1130623200 равно 2005-10-29 22:00:00 UTC

  • По московскому времени это так 2005-10-30 02:00:00 Europe/Moscow , однако это неоднозначно. Это может быть
  • либо 2005-10-30 02:00:00 04:00 Europe/Moscow MSD
  • или 2005-10-30 02:00:00 03:00 Europe/Moscow MSK

Время Unix 1130626800 равно 2005-10-29 23:00:00 UTC

  • По московскому времени это так 2005-10-30 02:00:00 Europe/Moscow , однако это неоднозначно. Это может быть
  • либо 2005-10-30 02:00:00 04:00 Europe/Moscow MSD
  • или 2005-10-30 02:00:00 03:00 Europe/Moscow MSK

Согласно документации Oracle, неоднозначное TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow' значение соответствует стандартному времени, т.Е. 2005-10-30 02:00:00 Europe/Moscow MSK 03:00 (как это было в 2005 году!)

Имейте в виду, в 2005 году московское стандартное время было MSK => 03:00 . В 2011 году российское правительство объявило, что переход на летнее время в будущем будет соблюдаться круглый год, что фактически заменит стандартное время. Т.Е. Сегодня московское стандартное время MSK => 04:00 называется MSD до 2011 года.

Проверьте с помощью

 SELECT 
    TO_CHAR(TIMESTAMP '2005-10-29 22:00:00 UTC' AT TIME ZONE 'Europe/Moscow', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS_1_UTC,
    TO_CHAR(TIMESTAMP '2005-10-29 23:00:00 UTC' AT TIME ZONE 'Europe/Moscow', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS_2_UTC,
    TO_CHAR(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS,
    TO_CHAR(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow MSK', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS_MSK,
    TO_CHAR(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow MSD', 'YYYY-MM-DD HH24:MI:SS TZH:TZM tzr TZD') AS TS_MSD
FROM dual
 
TS_1_UTC TS_2_UTC TS TS_MSK TS_MSD
2005-10-30 02:00:00 04:00 Европа /Moscow MSD 2005-10-30 02:00:00 03:00 Европа/Москва MSK 2005-10-30 02:00:00 03:00 Европа/Москва MSK 2005-10-30 02:00:00 03:00 Европа/Москва MSK 2005-10-30 02:00:00 04:00 Европа /Moscow MSD
 ALTER SESSION SET TIME_ZONE = 'Europe/Moscow';

SELECT 
    TO_CHAR(CAST(TIMESTAMP '2005-10-29 22:00:00 UTC' AT TIME ZONE 'Europe/Moscow' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS_1_UTC,
    TO_CHAR(CAST(TIMESTAMP '2005-10-29 23:00:00 UTC' AT TIME ZONE 'Europe/Moscow' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS_2_UTC,
    TO_CHAR(CAST(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS,
    TO_CHAR(CAST(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow MSK' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS_MSK,
    TO_CHAR(CAST(TIMESTAMP '2005-10-30 02:00:00 Europe/Moscow MSD' AS TIMESTAMP WITH LOCAL TIME ZONE), 'YYYY-MM-DD HH24:MI:SS TZD') AS TS_MSD
FROM dual
 
TS_1_UTC TS_2_UTC TS TS_MSK TS_MSD
2005-10-30 02:00:00 MSD 2005-10-30 02:00:00 MSK 2005-10-30 02:00:00 MSK 2005-10-30 02:00:00 MSK 2005-10-30 02:00:00 MSD

Я думаю, что Oracle делает это абсолютно правильно, независимо от того, есть ли у вас TIMESTAMP WITH TIME ZONE или TIMESTAMP WITH LOCAL TIME ZONE . Но вы должны тщательно проверить, что на самом деле вставлено в таблицу и как вы ее отображаете.