Как хранить дату и время и временные метки в часовом поясе UTC с JPA и Hibernate

Как настроить JPA / Hibernate для хранения даты / времени в базе данных как часовой пояс UTC (GMT)? Рассмотрим этот аннотированный объект JPA:

public class Event { @Id public int id; @Temporal(TemporalType.TIMESTAMP) public java.util.Date date; } 

Если дата 2008-фев-03 9:30 утра по тихоокеанскому стандартному времени (PST), то я хочу, чтобы время UTC с 2008 по февраль-03 5:30 вечера хранилось в базе данных. Аналогично, когда дата извлекается из базы данных, я хочу, чтобы она интерпретировалась как UTC. Таким образом, в этом случае 5:30 вечера составляет 5:30 вечера по UTC. Когда он будет отображаться, он будет отформатирован как 9:30 утра PST.

С Hibernate 5.2 вы можете принудительно установить часовой пояс UTC, используя следующее свойство конфигурации:

  

Для получения дополнительной информации ознакомьтесь с этой статьей .

Насколько я знаю, вам нужно разместить все приложение Java в часовой пояс UTC (чтобы Hibernate сохранил даты в UTC), и вам нужно будет преобразовать в любой часовой пояс, который требуется при отображении материала (по крайней мере, мы это делаем сюда).

При запуске мы делаем:

 TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC")); 

И установите желаемый часовой пояс для DateFormat:

 fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest")) 

Hibernate не знает о часовых поясах в Датах (потому что их нет), но на самом деле это слой JDBC, который вызывает проблемы. ResultSet.getTimestamp и PreparedStatement.setTimestamp говорят в своих документах, что они преобразуют даты в / из текущего часового пояса JVM по умолчанию при чтении и записи из / в базу данных.

Я придумал решение для этого в Hibernate 3.5 путем подclassа org.hibernate.type.TimestampType который заставляет эти методы JDBC использовать UTC вместо локального часового пояса:

 public class UtcTimestampType extends TimestampType { private static final long serialVersionUID = 8088663383676984635L; private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); @Override public Object get(ResultSet rs, String name) throws SQLException { return rs.getTimestamp(name, Calendar.getInstance(UTC)); } @Override public void set(PreparedStatement st, Object value, int index) throws SQLException { Timestamp ts; if(value instanceof Timestamp) { ts = (Timestamp) value; } else { ts = new Timestamp(((java.util.Date) value).getTime()); } st.setTimestamp(index, ts, Calendar.getInstance(UTC)); } } 

То же самое нужно сделать, чтобы исправить TimeType и DateType, если вы используете эти типы. Недостатком является то, что вам придется вручную указывать, что эти типы должны использоваться вместо значений по умолчанию в каждом поле «Дата» в ваших POJO (а также прерывает чистую совместимость JPA), если только кто-то не знает более общий метод переопределения.

UPDATE: Hibernate 3.6 изменил типы API. В 3.6 я написал class UtcTimestampTypeDescriptor, чтобы реализовать это.

 public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor { public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor(); private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); public  ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder( javaTypeDescriptor, this ) { @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) ); } }; } public  ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicExtractor( javaTypeDescriptor, this ) { @Override protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options ); } }; } } 

Теперь, когда приложение запускается, если вы установите TimestampTypeDescriptor.INSTANCE на экземпляр UtcTimestampTypeDescriptor, все временные метки будут сохранены и обработаны как находящиеся в UTC, без необходимости изменять annotations на POJO. [Я еще не тестировал это]

Добавляя ответ, который полностью основан на и обязанность divestoclimb с подсказкой от Shaun Stone. Просто хотелось подробно изложить это, потому что это общая проблема, и решение немного запутанно.

Это использование Hibernate 4.1.4.Final, хотя я подозреваю, что после 3.6 будет работать.

Сначала создайте utcTimestampTypeDescriptor divestoclimb

 public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor { public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor(); private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); public  ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder( javaTypeDescriptor, this ) { @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) ); } }; } public  ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicExtractor( javaTypeDescriptor, this ) { @Override protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options ); } }; } } 

Затем создайте UtcTimestampType, который использует метод UtcTimestampTypeDescriptor вместо TimestampTypeDescriptor как SqlTypeDescriptor в вызове супер-конструктора, но в остальном делегирует все в TimestampType:

 public class UtcTimestampType extends AbstractSingleColumnStandardBasicType implements VersionType, LiteralType { public static final UtcTimestampType INSTANCE = new UtcTimestampType(); public UtcTimestampType() { super( UtcTimestampTypeDescriptor.INSTANCE, JdbcTimestampTypeDescriptor.INSTANCE ); } public String getName() { return TimestampType.INSTANCE.getName(); } @Override public String[] getRegistrationKeys() { return TimestampType.INSTANCE.getRegistrationKeys(); } public Date next(Date current, SessionImplementor session) { return TimestampType.INSTANCE.next(current, session); } public Date seed(SessionImplementor session) { return TimestampType.INSTANCE.seed(session); } public Comparator getComparator() { return TimestampType.INSTANCE.getComparator(); } public String objectToSQLString(Date value, Dialect dialect) throws Exception { return TimestampType.INSTANCE.objectToSQLString(value, dialect); } public Date fromStringValue(String xml) throws HibernateException { return TimestampType.INSTANCE.fromStringValue(xml); } } 

Наконец, когда вы инициализируете конфигурацию Hibernate, зарегистрируйте UtcTimestampType в качестве переопределения типа:

 configuration.registerTypeOverride(new UtcTimestampType()); 

Теперь временные метки не должны касаться часового пояса JVM по пути в базу данных и из нее. НТН.

Вы думаете, что эта общая проблема будет позаботиться о спящем режиме. Но его нет! Есть несколько «хаков», чтобы понять это правильно.

Тот, который я использую, заключается в том, чтобы хранить дату в формате Long в базе данных. Поэтому я всегда работаю с миллисекундами после 1/1/70. Затем у меня есть получатели и сеттеры на моем classе, которые возвращают / принимают только Даты. Таким образом, API остается прежним. Нижняя сторона заключается в том, что у меня есть длинные базы данных. SO с SQL я могу в значительной степени выполнять операции <,>, = сравнения – не призрачные даты.

Другим подходом является пользовательский тип сопоставления, как описано здесь: http://www.hibernate.org/100.html

Я думаю, что правильный способ справиться с этим – использовать Календарь вместо Даты. С помощью Calendar вы можете установить TimeZone перед сохранением.

ПРИМЕЧАНИЕ. Глупый stackoverflow не позволит мне прокомментировать, так что вот ответ на david a.

Если вы создадите этот объект в Чикаго:

 new Date(0); 

Hibernate сохраняет это как «31.12.1969 18:00:00». Даты должны быть лишены часовой пояс, поэтому я не уверен, почему будет произведена корректировка.

Здесь действуют несколько часовых поясов:

  1. Классы Java Java (util и sql), которые имеют неявные временные интервалы UTC
  2. Часовой пояс, на котором работает JVM, и
  3. часовой пояс по умолчанию для вашего сервера базы данных.

Все они могут быть разными. Hibernate / JPA имеет серьезный недостаток в дизайне, поскольку пользователь не может легко обеспечить сохранение информации о часовом поясе на сервере базы данных (что позволяет восстановить правильные времена и даты в JVM).

Без возможности (легко) хранить часовой пояс с помощью JPA / Hibernate, информация теряется, и как только информация теряется, становится дорого ее строить (если это вообще возможно).

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

Извините, это сообщение не обеспечивает обход (это было отвечено в другом месте), но это рационализация, почему важно всегда хранить информацию о часовом поясе. К сожалению, многие компьютерные ученые и программисты не согласны с необходимостью делать часовые пояса просто потому, что они не понимают перспективы «потери информации» и как это очень затрудняет такие вещи, как интернационализация, что очень важно в наши дни с веб-сайтами, доступными клиентов и людей в вашей организации, поскольку они перемещаются по всему миру.

Пожалуйста, посмотрите мой проект на Sourceforge, который имеет типы пользователей для стандартных типов даты и времени SQL, а также JSR 310 и Joda Time. Все типы пытаются решить проблему смещения. См. http://sourceforge.net/projects/usertype/

EDIT: В ответ на вопрос Дерека Махара прилагается к этому комментарию:

«Крис, ваши типы пользователей работают с Hibernate 3 или выше? – Дерек Махар 7 ноября 2010 года в 12:30″

Да, эти типы поддерживают версии Hibernate 3.x, включая Hibernate 3.6.

Дата не находится в каком-либо часовом поясе (это миллисекундный офис с определенного момента времени, такой же для всех), но базовые (R) DB обычно хранят временные метки в политическом формате (год, месяц, день, час, минута, секунда,. ..), который чувствителен к часовому поясу.

Чтобы быть серьезным, Hibernate ДОЛЖНО быть разрешено сообщать в какой-либо форме сопоставления, что дата DB находится в таком-то часовом поясе, так что когда она загружает или хранит ее, она не принимает на себя …

Hibernate не позволяет указывать часовые пояса по annotations или любым другим способом. Если вы используете календарь вместо даты, вы можете реализовать обходное решение с использованием свойства HIbernate AccessType и реализовать само отображение. Более продвинутое решение заключается в реализации пользовательского UserType для сопоставления даты или календаря. Оба решения объясняются в этом блоге: http://dev-metal.blogspot.com/2010/11/mapping-dates-and-time-zones-with.html

Я столкнулся с той же проблемой, когда хотел сохранить даты в БД в формате UTC и избегать использования varchar и явных преобразований String <-> java.util.Date или установки всего приложения Java в часовой пояс UTC (потому что это может приводят к другим неожиданным проблемам, если JVM используется во многих приложениях).

Итак, есть проект DbAssist с открытым исходным DbAssist , который позволяет вам легко исправить чтение / запись в качестве даты UTC из базы данных. Поскольку вы используете JPA Annotations для сопоставления полей в сущности, все, что вам нужно сделать, это включить следующую зависимость к вашему файлу maven pom :

  com.montrosesoftware DbAssist-5.2.2 1.0-RELEASE  

Затем вы применяете исправление (для примера Hibernate + Spring Boot), добавляя аннотацию @EnableAutoConfiguration до classа приложения Spring. Для других инструкций по установке и других примеров использования просто обратитесь к github проекта.

Хорошо, что вам вообще не нужно изменять сущности; вы можете оставить свои поля java.util.Date такими, какие они есть.

5.2.2 должен соответствовать версии Hibernate, которую вы используете. Я не уверен, какую версию вы используете в своем проекте, но полный список предоставленных исправлений доступен на странице вики проекта github проекта. Причина, по которой исправление отличается для разных версий Hibernate, заключается в том, что создатели Hibernate несколько раз изменяли API между выпусками.

Внутри исправление использует подсказки от divestoclimb, Shane и нескольких других источников, чтобы создать пользовательский UtcDateType . Затем он сопоставляет стандартный java.util.Date с пользовательским UtcDateType который обрабатывает всю необходимую обработку часового пояса. Отображение типов достигается с @Typedef annotations @Typedef в предоставленном файле package-info.java .

 @TypeDef(name = "UtcDateType", defaultForType = Date.class, typeClass = UtcDateType.class), package com.montrosesoftware.dbassist.types; 

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

  • Как преобразовать время в часовой пояс устройства iPhone?
  • Получить имя часового пояса
  • Как использовать TimeZoneInfo для получения местного времени во время летнего времени?
  • Как правильно установить JVM TimeZone
  • Как изменить часовой пояс mysql в java-соединении
  • Названия часовых поясов с одинаковыми свойствами дают разные результаты при применении к метке времени
  • Игнорирование часовых поясов в Rails и PostgreSQL
  • Как конвертировать datetime в timestamp с помощью C # /. NET (игнорируя текущий часовой пояс)
  • Как установить часовой пояс MySQL?
  • Как получить часовой пояс из местоположения с использованием координат широты и долготы?
  • Как получить текущую дату и время вашего часового пояса в Java?
  • Давайте будем гением компьютера.