Установить часовой пояс UTC, который уже находится в UTC

У меня есть приложение, в котором я установил часовой пояс UTC следующим образом:

// set default time zone to UTC
TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC));

Это хорошо для всех полей даты, которые мы храним в базе данных как UTC. Но одна дата уже наступает в UTC, поэтому выше часовой пояс снова установит ее в UTC.

Как избежать повторной настройки в UTC?

Некоторый код

public static void main(String[] args) throws DatatypeConfigurationException {
        DatatypeFactory datatypeFactory  = DatatypeFactory.
         newInstance();
        // set default time zone to UTC
        TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC));
        // coming from external program in UTC but assigned it here for reference
        long someTimeToBeInUTC = 1528371000000L;
        GregorianCalendar start = new GregorianCalendar();
        start.setTimeInMillis(someTimeToBeInUTC);
        XMLGregorianCalendar startXmlCalendar  = 
        datatypeFactory.newXMLGregorianCalendar(start);
        System.out.println(startXmlCalendar.
        toGregorianCalendar().getTime());

    }

Думай, как будто у меня есть момент времени someTimeToBeInUTC независимо от часового пояса, а затем из-за линии TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)) это устанавливает это в UTC.

Затем в hibernate у меня есть свойство в hbm.xml таблицы, где я храню это значение:

<property name="startTime" type="timestamp">
            <column name="start_time" length="29" />
        </property>

Тип startTime- это метка времени без часового пояса в postgres. Поэтому, когда мы сохраняем, он снова конвертирует его в UTC.

2 ответа

ТЛ; др

Кажется, ты слишком много работаешь.

Вы используете неправильные классы. Вместо этого используйте современные классы java.time.

myPreparedStatement.setObject(
    … , 
    Instant.ofEpochMilli( 1_528_371_000_000L ) 
)

Вы используете неверный тип данных в вашей базе данных. TIMEZONE WITHOUT TIME ZONE Тип не может быть использован для хранения определенного момента.

Избегайте установки часового пояса по умолчанию

TimeZone.setDefault

Этот вызов немедленно влияет на весь код во всех потоках всех приложений в JVM! Так что делайте этот призыв только в самых отчаянных обстоятельствах.

Вместо этого передайте желаемый / ожидаемый часовой пояс как дополнительный ZoneId аргумент для многих методов java.time.

Часовой пояс по умолчанию вашей операционной системы и вашей JVM не должен иметь никакого отношения к вашему коду. Укажите явно желаемый / ожидаемый часовой пояс как ZoneId (или же ZoneOffset) объект.

Избегайте устаревших классов даты и времени.

Старые классы даты и времени, связанные с ранними версиями Java, ужасны. Это включает в себя GregorianCalendar класс видно в вашем коде. Вместо этого используйте их замену, современные классы java.time.

В частности, GregorianCalendar заменяется ZonedDateTime,

Instant

Instant Класс в java.time является основным строительным блоком. Instant класс представляет момент на временной шкале в UTC с разрешением наносекунд (до девяти (9) цифр десятичной дроби).

Instant instant = Instant.now() ;  // Capture the current moment in UTC.

Видимо, у вас есть счет в миллисекундах с начала отсчета эпохи первого момента 1970 UTC, 1970-01-01T00: 00: 00Z. Просто проанализируйте этот номер как Instant возражать ofEpochMilli,

long input = 1_528_371_000_000L ;
Instant instant = Instant.ofEpochMilli( input ) ;

instant.toString(): 2018-06-07T11:30:00

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

Вы можете представить Instant пользователю в определенном часовом поясе. Применить ZoneId чтобы получить ZonedDateTime,

Укажите правильное название часового пояса в формате continent/region, такие как America/Montreal, Africa/Casablanca, или же Pacific/Auckland, Никогда не используйте 3-4-буквенные псевдозоны, такие как EST или же IST поскольку они не являются истинными часовыми поясами, не стандартизированы и даже не уникальны (!).

ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;  // Or Europe/Paris, Africa/Tunis, etc.

База данных

Я не знаю, что вы делаете с XMLGregorianCalendar, И я не могу обратиться к Hibernate, так как я не пользователь, хотя я знаю, что Hibernate поддерживает типы java.time.

Но если вы пытаетесь записать и извлечь момент с Postgres, вы слишком много работаете.

Сохранение момента в UTC в столбец типа TIMESTAMP WTH TIME ZONE (не WITHOUT! - увидеть ниже).

String sql = "INSERT INTO tbl ( event ) VALUES ( ? ) ;" ;       // Writing a moment into a column of type `TIMESTAMP WTH TIME ZONE`.
…
Instant instant = Instant.ofEpochMilli( 1_528_371_000_000L ) ;  // Representing a moment in UTC.
myPreparedStatement.setObject( 1 , instant ) ;                  // As of JDBC 4.2 and later, we can directly exchange java.time objects with our database.

Индексирование.

Instant instant = myResultSet.getObject( … , Instant.class ) ;

Имейте в виду, что стандарт SQL едва затрагивает тему даты и времени. При работе с датой и временем разные базы данных ведут себя очень по- разному.

Postgres работает так: TIMESTAMP WITH TIME ZONE это что-то неправильно. Часовой пояс не сохраняется как часть метки времени. Любая предоставленная временная зона информации о смещении от UTC, представленная с помощью ввода, используется для настройки на UTC. Значение UTC записывается в базу данных. Зона / смещение затем отбрасывается / забывается. Если вам важно запомнить исходную зону / смещение ввода, вы должны сохранить ее отдельно в другом столбце.

При получении TIMESTAMP WITH TIME ZONE значение из Postgres, вы всегда получаете значение в UTC. Это может быть неочевидно для вас, если ваш инструмент промежуточного программного обеспечения, соединяющий вас (ваше приложение) с базой данных, вводит собственное мнение относительно желаемого часового пояса для презентации.

Избежать путаницы с часовыми поясами легко с Java: Pass Instant объекты и получить Instant объекты. Instant всегда в UTC. Так зовет ResultSet::getObject( … , Instant.class) всегда будет верно представлять базу данных TIMESTAMP WITH TIME ZONE значение.

Одним из хитрых моментов является разрешение доли секунды. Классы java.time используют разрешение наносекунд, а Postgres - микросекунды. Таким образом, вы можете усечь Instant явно в вашем коде, чтобы признать этот факт. Ваш драйвер JDBC будет урезан для вас, но мне не нравятся такие закулисные манипуляции с данными.

Instant instant = Instant.now().truncatedTo( ChronoUnit.MILLISECONDS ) ;  // Lop off any nanoseconds to match Postgres storing microseconds.

Неправильный тип данных в вашей колонке

Тип startTime - это метка времени без часового пояса в postgres.

TIMESTAMP WITHOUT TIME ZONE неправильный тип данных для хранения момента. В этом типе отсутствует понятие часового пояса или смещения от UTC. Так что это не может представлять конкретный момент. Он представляет собой набор потенциальных моментов в диапазоне около 26-27 часов (диапазон всех часовых поясов).

Вместо этого вы должны использовать TIMESTAMP WITH TIME ZONE Тип для хранения определенного момента на временной шкале.

Поэтому, когда мы сохраняем, он снова конвертирует его в UTC.

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

Тип SQL-стандарт TIMEZONE WITHOUT TIME ZONE эквивалентно типу java.time LocalDateTime,

Вы должны использовать эти типы без зоны только в следующих трех ситуациях:

  • Зона или смещение неизвестны.
    Это плохо. Это неверные данные. Аналогично наличию цены / стоимости без знания валюты. Вы должны отвергать такие данные, а не хранить их.
  • Намерение "везде", как в каждом часовом поясе.
    Например, корпоративная политика, которая гласит: "Все наши фабрики будут работать на обед в 12:30" означает, что фабрика в Дели сломается за несколько часов до фабрики в Дюссельдорфе, которая ломается за несколько часов до фабрики в Детройте.
  • Намечен определенный момент в будущем, но мы боимся, что политики переопределят часовой пояс.
    Правительства меняют правила своих часовых поясов с удивительной частотой и с удивительно небольшим предупреждением или даже без предупреждения вообще. Так что, если вы хотите записаться на прием в 3 часа дня на определенную дату, и вы действительно имеете в виду 3 часа дня, независимо от какого-либо сумасшедшего решения, которое правительство может принять за это время, сохраните LocalDateTime, Чтобы распечатать отчет или отобразить календарь, динамически примените часовой пояс (ZoneId) генерировать конкретный момент (ZonedDateTime или же Instant). Это должно быть сделано на лету, а не хранить значение.

О java.time

Инфраструктура java.time встроена в Java 8 и более поздние версии. Эти классы вытесняют проблемные старые классы даты и времени, такие как java.util.Date, Calendar & SimpleDateFormat,

Проект Joda-Time, находящийся сейчас в режиме обслуживания, рекомендует перейти на классы java.time.

Чтобы узнать больше, смотрите Oracle Tutorial. И поиск переполнения стека для многих примеров и объяснений. Спецификация JSR 310.

Вы можете обмениваться объектами java.time напрямую с вашей базой данных. Используйте драйвер JDBC, соответствующий JDBC 4.2 или более поздней версии. Нет необходимости в строках, нет необходимости в java.sql.* классы.

Где взять классы java.time?

  • Java SE 8, Java SE 9, Java SE 10 и более поздние версии
    • Встроенный.
    • Часть стандартного Java API со встроенной реализацией.
    • Java 9 добавляет некоторые незначительные функции и исправления.
  • Java SE 6 и Java SE 7
    • Большая часть функциональности java.time перенесена на Java 6 и 7 в ThreeTen-Backport.
  • Android

Проект ThreeTen-Extra расширяет java.time дополнительными классами. Этот проект является полигоном для возможных будущих дополнений к java.time. Вы можете найти некоторые полезные классы здесь, такие как Interval, YearWeek, YearQuarter и многое другое.

Время в миллисекундах не имеет ничего общего с часовым поясом.

Время в миллисекундах является точным моментом времени независимо от того, где вы используете его в объектах Calendar, Date или Timestamp.

Пожалуйста, внимательно прочитайте документацию по Java. Он всегда показывает миллисекунды, прошедшие с эпохи (01.01.1970 00:00:00 UTC).

Часовой пояс действует только тогда, когда другое представление используется из этих объектов. например, из методов toString(), получить поля из календаря и т. д.)

ОБНОВЛЕНИЕ Допустим, у вас есть время в миллисекундах, как 1528371250000L...

тогда у вас может быть два Календаря (они осведомлены о TimeZone) один в UTC один, в восточном

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

public static void main(String... args) throws Exception {

    Calendar utcCalendar = Calendar
            .getInstance(TimeZone.getTimeZone("UTC"));

    utcCalendar.setTimeInMillis(1528371250000L);

    System.out.println("UTC Calendar: \t\t" + (utcCalendar.get(Calendar.MONTH) +1)
            + "/" + utcCalendar.get(Calendar.DAY_OF_MONTH) + "/"
            + utcCalendar.get(Calendar.YEAR) + " "
            + utcCalendar.get(Calendar.HOUR_OF_DAY) + ":"
            + utcCalendar.get(Calendar.MINUTE) + ":"
            + utcCalendar.get(Calendar.SECOND) + " " + utcCalendar.getTimeZone().getID());

    Calendar eastCalendar = Calendar.getInstance(TimeZone
            .getTimeZone("EST"));

    eastCalendar.setTimeInMillis(1528371250000L);

    System.out.println("Eastern Calendar: \t"
            + (eastCalendar.get(Calendar.MONTH)+1) + "/"
            + eastCalendar.get(Calendar.DAY_OF_MONTH) + "/"
            + eastCalendar.get(Calendar.YEAR) + " "
            + eastCalendar.get(Calendar.HOUR_OF_DAY) + ":"
            + eastCalendar.get(Calendar.MINUTE) + ":"
            + eastCalendar.get(Calendar.SECOND) + " " + eastCalendar.getTimeZone().getID());

    XMLGregorianCalendar xmlUtcCalendar = DatatypeFactory.newInstance()
            .newXMLGregorianCalendar((GregorianCalendar) utcCalendar);

    System.out.println("XML UTC Calendar: \t" + xmlUtcCalendar.toString());

    XMLGregorianCalendar xmlEastCalendar = DatatypeFactory.newInstance()
            .newXMLGregorianCalendar((GregorianCalendar) eastCalendar);

    System.out.println("XML Eastern Calendar: \t" + xmlEastCalendar.toString());

}

Выход:

 UTC Calendar:          6/7/2018 11:34:10 UTC
 Eastern Calendar:      6/7/2018 6:34:10 EST
 XML UTC Calendar:      2018-06-07T11:34:10.000Z
 XML Eastern Calendar:  2018-06-07T06:34:10.000-05:00
Другие вопросы по тегам