it-swarm.com.ru

LocalDateTime to ZonedDateTime

У меня есть Java 8 веб-приложение Spring, которое будет поддерживать несколько регионов. Мне нужно сделать события календаря для местоположения клиента. Допустим, мой веб-сервер и сервер Postgres размещены в часовом поясе MST (но я полагаю, это может быть где угодно, если мы перейдем в облако.) Но клиент находится в EST. Поэтому, следуя некоторым рекомендациям, которые я прочитал, я подумал, что буду хранить все даты в формате UTC. Все поля даты и времени в базе данных объявляются как TIMESTAMP.

Итак, вот как я беру LocalDateTime и конвертирую в UTC:

ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(dto.getStartDateTime(), ZoneOffset.UTC, null);
//appointment startDateTime is a LocalDateTime
appointment.setStartDateTime( startZonedDT.toLocalDateTime() );

Теперь, например, когда начинается поиск по дате, мне нужно преобразовать запрошенное время в UTC, получить результаты и преобразовать в часовой пояс пользователя (хранится в БД):

ZoneId  userTimeZone = ZoneId.of(timeZone.getID());
ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(appointment.getStartDateTime(), userTimeZone, null);
dto.setStartDateTime( startZonedDT.toLocalDateTime() );

Я не уверен, что это правильный подход. Мне также интересно, если из-за перехода с LocalDateTime на ZonedDateTime и наоборот, я могу потерять информацию о часовом поясе.

Вот то, что я вижу, и это не кажется мне правильным. Когда я получаю LocalDateTime от пользовательского интерфейса, я получаю это:

2016-04-04T08:00

The ZonedDateTime =

dateTime=2016-04-04T08:00
offset="Z"
zone="Z"

И затем, когда я назначаю это преобразованное значение моему назначению LocalDateTime, я сохраняю:

2016-04-04T08:00

Мне кажется, потому что я храню в LocalDateTime, я теряю часовой пояс, который я преобразовал в ZonedDateTime.

Должен ли я заставить мою сущность (встречу) использовать ZonedDateTime вместо LocalDateTime, чтобы Postgres не терял эту информацию?

---------------- Редактировать --------------- -

Получив отличный ответ от Basils, я понял, что могу позволить себе роскошь не заботиться о часовом поясе пользователей - все встречи относятся к определенному месту, поэтому я могу сохранить все даты в формате UTC, а затем преобразовать их в часовой пояс места при получении. Я сделал следующее продолжение вопрос

25
sonoerin

Postgres не имеет такого типа данных, как TIMESTAMP. Postgres имеет два типа для даты плюс время суток: TIMESTAMP WITH TIME ZONE а также TIMESTAMP WITHOUT TIME ZONE. Эти типы имеют очень разное поведение в отношении информации о часовом поясе.

  • Тип WITH использует любую информацию о смещении или часовом поясе для настройки даты и времени на [~ # ~] utc [~ # ~] , затем удаляет это смещение или часовой пояс; Postgres никогда не сохраняет информацию о смещении/зоне.
    • Этот тип представляет момент, конкретную точку на временной шкале.
  • Тип WITHOUT игнорирует любую информацию о смещении или зоне, которая может присутствовать.
    • Этот тип нет представляет момент. Это представляет смутное представление о потенциал моменты в диапазоне около 26-27 часов (диапазон часовых поясов по всему миру).

Вы фактически всегда хотите тип WITH, как объяснил здесь эксперт Дэвид Э. Уилер. WITHOUT имеет смысл только тогда, когда вы имеете смутное представление о дате-времени, а не о фиксированной точке на временной шкале. Например, "Рождество в этом году начинается в 2016-12-25T00: 00: 00" будет сохранено в WITHOUT, как это относится к любому часовому поясу, еще не имея был применен к любому часовому поясу, чтобы получить фактический момент времени. Если эльфы Санты отслеживали время начала Юджина Орегона США, то они использовали бы тип WITH и ​​вход, включающий смещение или часовой пояс, например 2016-12-25T00:00:00-08:00, который сохраняется в Postgres как 2016-12-25T08:00.00Z (где Z означает зулу или UTC).

Эквивалент Postgres 'TIMESTAMP WITHOUT TIME ZONE в Java.time это Java.time.LocalDateTime. Поскольку вы намеревались работать в UTC (хорошая вещь), вы должны нет использовать LocalDateTime (плохая вещь). Это может быть основной причиной путаницы и неприятностей для вас. Вы продолжаете думать об использовании LocalDateTime или ZonedDateTime, но вам не следует использовать ни один из них; вместо этого вы должны использовать Instant (обсуждается ниже).

Мне также интересно, если из-за перехода с LocalDateTime на ZonedDateTime и наоборот, я могу потерять любую информацию о часовом поясе.

На самом деле вы есть. Весь смысл LocalDateTime заключается в потерять информация о часовом поясе. Так что мы редко используем этот класс в большинстве приложений. Опять рождественский пример. Или другой пример: "Политика компании: все наши заводы по всему миру обедают в 12:30". Это будет LocalTime, а для конкретной даты LocalDateTime. Но это не имеет реального значения, а не фактической точки на временной шкале, пока вы не примените часовой пояс, чтобы получить ZonedDateTime . Этот перерыв на обед будет в разные моменты времени на заводе в Дели, чем в Дюссельдорфе, и снова на заводе в Детройте.

Слово "Локальный" в LocalDateTime может быть нелогичным, так как оно означает не особо локальность. Когда вы читаете "Местное" в названии класса, подумайте: "Не мгновение… не на временной шкале… просто размытое представление о дате-времени своего рода сорта".

Ваши серверы почти всегда должны быть настроены на UTC в часовом поясе их операционной системы. Но ваше программирование никогда не должно зависеть от этого внешнего вида, поскольку системному администратору слишком просто изменить его или любому другому приложению Java) изменить текущий часовой пояс по умолчанию в JVM. Поэтому всегда указывайте Ваш желаемый/ожидаемый часовой пояс. (Кстати, то же самое касается Locale.)

Upshot:

  • Вы слишком усердно работаете.
  • Программисты/системные администраторы должны научиться "Думать глобально, представлять локально".

В течение рабочего дня, надевая каску своего гика, думайте в UTC. Только в конце дня, когда вы переключаетесь на работу вашего мирянина, вы можете вернуться к размышлению о местном времени вашего города.

Ваша бизнес-логика должна быть ориентирована на UTC. Хранение вашей базы данных, бизнес-логика, обмен данными, сериализация, ведение журнала и ваше собственное мышление должны выполняться в часовом поясе UTC (и, между прочим, 24-часовые часы). При представлении данных пользователям только тогда примените определенный часовой пояс. Думайте о зонированных датах как о внешней вещи, а не как о рабочей части вашего приложения.

На стороне Java используйте Java.time.Instant (момент времени на UTC) в большей части вашей бизнес-логики.

Instant now = Instant.now();

Надеемся, что драйверы [~ # ~] jdbc [~ # ~] будут в конечном итоге обновлены для непосредственного взаимодействия с типами Java.time, такими как Instant. До этого мы должны использовать типы Java.sql. В старом классе Java.sql есть новые методы для преобразования в/из Java.time.

Java.sql.TimeStamp ts = Java.sql.TimeStamp.valueOf( instant );

Теперь передайте это Java.sql.TimeStamp объект через setTimestamp в PreparedStatement для сохранения в столбце, определенном как TIMESTAMP WITH TIME ZONE в Postgres.

Чтобы пойти в другом направлении:

Instant instant = ts.toInstant();

Так что это легко, переходя от Instant к Java.sql.Timestamp до TIMESTAMP WITH TIME ZONE, все в UTC. Часовые пояса не задействованы. Текущий часовой пояс по умолчанию вашей серверной ОС, вашей JVM и ваших клиентов не имеет значения.

Чтобы представить пользователю, примените часовой пояс. Используйте правильные имена часовых поясов , никогда не используйте 3-4-буквенные коды, такие как EST или IST.

ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

Вы можете настроить в другой зоне по мере необходимости.

ZonedDateTime zdtKolkata = zdt.withZoneSameInstant( ZoneId.of( "Asia/Kolkata" ) );

Чтобы вернуться к Instant, моменту времени на UTC, вы можете извлечь его из ZonedDateTime.

Instant instant = zdt.toInstant();

Мы не использовали там LocalDateTime .

Если вы получите часть данных без какого-либо смещения от UTC или часового пояса, например 2016-04-04T08:00, эти данные совершенно бесполезны для вас (при условии, что мы не говорим о сценариях типа рождественского или корпоративного обеда, которые обсуждались выше). Дата-время без информации о смещении/зоне похожа на денежную сумму без указания валюты: 142.70 или даже $142.70 -- бесполезный. Но USD 142.70, или CAD 142.70, или MXN 142.70… Это полезно.

Если вы получите это 2016-04-04T08:00 значение, и вы абсолютно уверены предполагаемого контекста/контекста зоны, затем:

  1. Разобрать эту строку как LocalDateTime.
  2. Примените смещение от UTC, чтобы получить OffsetDateTime, или (лучше) примените часовой пояс, чтобы получить ZonedDateTime.

Понравился этот код.

LocalDateTime ldt = LocalDateTime.parse( "2016-04-04T08:00" );
ZoneId zoneId = ZoneId.of( "Asia/Kolkata" ); // Or "America/Montreal" etc.
ZonedDateTime zdt = ldt.atZone( zoneId ); // Or atOffset( myZoneOffset ) if only an offset is known rather than a full time zone.

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

JDBC 4.2

Начиная с JDBC 4.2 мы можем напрямую обмениваться объектами Java.time с базой данных. Нет необходимости использовать Java.sql.Timestamp снова, ни связанные с ним классы.

Хранение с использованием OffsetDateTime , как определено в спецификации JDBC.

myPreparedStatement.setObject( … , instant.atOffset( ZoneOffset.UTC ) ) ;  // The JDBC spec requires support for `OffsetDateTime`. 

... или, возможно, используйте Instant напрямую, если это поддерживается вашим драйвером JDBC.

myPreparedStatement.setObject( … , instant ) ;  // Your JDBC driver may or may not support `Instant` directly, as it is not required by the JDBC spec. 

Получение.

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;

О 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 API со встроенной реализацией.
    • Java 9 добавляет некоторые незначительные функции и исправления.
  • Java SE 6 и Java SE 7
    • Большая часть функциональности Java.time перенесена на Java 6 & 7 in ThreeTen-Backport .
  • Android [. .____]
    • Более поздние версии Android связывают реализации классов Java.time.
    • Для более ранних Android (<26), ThreeTenABP проект адаптируется ThreeTen-Backport (упомянуто выше). Смотрите Как использовать ThreeTenABP… .

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

66
Basil Bourque