Как сохранить дату / время и временные метки в часовом поясе UTC с помощью JPA и Hibernate
Как я могу настроить JPA/Hibernate для хранения даты / времени в базе данных в качестве часового пояса UTC (GMT)? Рассмотрим эту аннотированную сущность JPA:
public class Event {
@Id
public int id;
@Temporal(TemporalType.TIMESTAMP)
public java.util.Date date;
}
Если дата - 9 февраля 2008 года, 9:30 утра по тихоокеанскому стандартному времени (PST), то я хочу, чтобы в базе данных было записано время UTC 2008 года - 3 февраля, 17:30. Аналогично, когда дата извлекается из базы данных, я хочу, чтобы она интерпретировалась как UTC. Таким образом, в этом случае 5:30 вечера - это 5:30 вечера UTC. Когда он отобразится, он будет отформатирован как 9:30 утра по тихоокеанскому времени.
13 ответов
С Hibernate 5.2 теперь вы можете форсировать часовой пояс UTC, используя следующее свойство конфигурации:
<property name="hibernate.jdbc.time_zone" value="UTC"/>
Для более подробной информации, проверьте эту статью.
Насколько мне известно, вам нужно поместить все ваше Java-приложение в часовой пояс UTC (чтобы Hibernate будет хранить даты в UTC), и вам нужно будет конвертировать в любой часовой пояс, когда вы хотите что-то отобразить (по крайней мере, мы это делаем сюда).
При запуске мы делаем:
TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
И установите желаемый часовой пояс для DateFormat:
fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
Hibernate не знает о часовых поясах в Dates (потому что их нет), но на самом деле это вызывает уровень JDBC, который вызывает проблемы. ResultSet.getTimestamp
а также PreparedStatement.setTimestamp
оба говорят в своих документах, что они по умолчанию преобразовывают даты в / из текущего часового пояса JVM при чтении и записи из / в базу данных.
Я придумал решение для этого в Hibernate 3.5 путем создания подклассов 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, если вы используете эти типы. Недостатком является то, что вам придется вручную указывать, что эти типы должны использоваться вместо значений по умолчанию в каждом поле Date в ваших POJO (а также нарушает чистую совместимость с JPA), если кто-то не знает о более общем методе переопределения.
ОБНОВЛЕНИЕ: Hibernate 3.6 изменил API типов. В 3.6 я написал класс UtcTimestampTypeDescriptor для реализации этого.
public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor {
public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor();
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicBinder<X>( 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 <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicExtractor<X>( 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 без необходимости изменения аннотаций в POJO. [Я еще не проверял это]
Используя Spring Boot JPA, используйте приведенный ниже код в файле application.properties, и, очевидно, вы можете изменить часовой пояс по своему выбору.
spring.jpa.properties.hibernate.jdbc.time_zone = UTC
Затем в вашем файле класса Entity,
@Column
private LocalDateTime created;
Добавление ответа, полностью основанного на divestoclimb, с подсказкой от Shaun Stone. Просто хотел объяснить это подробно, так как это общая проблема, а решение немного запутанно.
Это использует Hibernate 4.1.4.Final, хотя я подозреваю, что что-то после 3.6 будет работать.
Во-первых, создайте divestoclimb's UtcTimestampTypeDescriptor
public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor {
public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor();
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicBinder<X>( 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 <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicExtractor<X>( 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<Date>
implements VersionType<Date>, LiteralType<Date> {
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<Date> 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 на пути к базе данных и из нее. НТН.
Можно подумать, что об этой общей проблеме позаботится Hibernate. Но это не так! Есть несколько "хаков", чтобы понять это правильно.
Тот, который я использую, чтобы сохранить дату как длинную в базе данных. Поэтому я всегда работаю с миллисекундами после 1 января 70 года. Затем в моем классе есть геттеры и сеттеры, которые возвращают / принимают только даты. Таким образом, API остается прежним. Недостатком является то, что у меня есть длинные в базе данных. Так что с SQL я могу делать только сравнения <,>,=, а не причудливые операторы дат.
Другой подход заключается в использовании пользовательского типа отображения, как описано здесь: http://www.hibernate.org/100.html
Я думаю, что правильный способ справиться с этим - использовать календарь вместо даты. С помощью календаря вы можете установить часовой пояс перед сохранением.
ПРИМЕЧАНИЕ. Глупый стековый поток не позволит мне комментировать, поэтому вот ответ Дэвиду а.
Если вы создаете этот объект в Чикаго:
new Date(0);
Hibernate сохраняет его как "31.12.1969 18:00:00". Даты должны быть лишены часового пояса, поэтому я не уверен, почему будет произведена корректировка.
Здесь действует несколько часовых поясов:
- Java-классы Date (util и sql), которые имеют неявные часовые пояса UTC
- Часовой пояс, в котором работает ваша JVM, и
- часовой пояс по умолчанию вашего сервера базы данных.
Все они могут быть разными. Hibernate/JPA имеет серьезный недостаток проекта в том, что пользователь не может легко обеспечить сохранение информации о часовом поясе на сервере базы данных (что позволяет восстанавливать правильное время и даты в JVM).
Без возможности (легко) сохранять часовой пояс с помощью JPA/Hibernate информация теряется, а после потери информации ее создание становится дорогостоящим (если это вообще возможно).
Я бы сказал, что лучше всегда хранить информацию о часовом поясе (должно быть по умолчанию), и тогда пользователи должны иметь дополнительную возможность оптимизировать часовой пояс (хотя это действительно влияет только на отображение, в любой дате все еще существует неявный часовой пояс).
Извините, этот пост не предлагает обходного пути (об этом уже было сказано в другом месте), но это рационализация того, почему всегда важно хранить информацию о часовом поясе. К сожалению, кажется, что многие компьютерные ученые и практики программирования спорят о необходимости часовых поясов просто потому, что они не ценят перспективу "потери информации" и то, как это делает такие вещи, как интернационализация, очень трудными - что очень важно в наши дни, когда веб-сайты доступны клиенты и люди в вашей организации, которые перемещаются по всему миру.
Пожалуйста, взгляните на мой проект на Sourceforge, в котором есть пользовательские типы для стандартных типов даты и времени SQL, а также JSR 310 и Joda Time. Все типы пытаются решить проблему смещения. Смотрите http://sourceforge.net/projects/usertype/
РЕДАКТИРОВАТЬ: В ответ на вопрос Дерека Махара, прикрепленный к этому комментарию:
"Крис, твои пользовательские типы работают с Hibernate 3 или выше? - Дерек Махар 7 ноября 2010 года в 12:30"
Да, эти типы поддерживают версии Hibernate 3.x, включая Hibernate 3.6.
Дата не находится ни в одном часовом поясе (это миллисекундный офис с определенного момента времени, одинакового для всех), но базовые (R) БД обычно хранят временные метки в политическом формате (год, месяц, день, час, минута, секунда и т. Д.)...) это чувствительно к часовому поясу.
Чтобы быть серьезным, Hibernate ДОЛЖЕН быть разрешен в той или иной форме отображения, что дата БД находится в таком-то и таком-то часовом поясе, чтобы при загрузке или сохранении она не принимала свое собственное...
Сейчас 2023 год. С точки зрения Java вы хотите сохранить файл , верно?
Так что просто сделайте это:
public class Event {
@Id
public int id;
@Column("event_timestamp")
public Instant eventTimestamp;
}
Это будет работать с Hibernate 5.2 или более поздней версии. В Интернете можно найти множество историй о том, как приходилось устанавливать
К счастью, мир намного проще (сейчас), чем позволяют вам поверить другие ответы на этой странице.
Вот полный контрольный список для правильного хранения дат в вашей базе данных:
- во-первых, убедитесь, что часовой пояс вашей ОС установлен правильно: либо установите переменную TZ env в вашем файле Docker, либо запустите приложение Java с параметром -Duser.timezone="UTC" .
- Как упоминалось ранее: также установите часовой пояс для JPA с помощью hibernate.jdbc.time_zone = UTC.
- Используйте OffsetDateTime для хранения дат в формате UTC в ваших сущностях. Не используйте Date, Calendar и ZonedDateTime !
Hibernate не позволяет указывать часовые пояса с помощью аннотаций или любых других средств. Если вы используете Календарь вместо даты, вы можете реализовать обходной путь, используя свойство AccessType HIbernate и реализуя сопоставление самостоятельно. Более сложным решением является реализация пользовательского типа пользователя для сопоставления даты или календаря. Оба решения описаны в этом блоге: http://dev-metal.blogspot.com/2010/11/mapping-dates-and-time-zones-with.html
Я столкнулся с той же проблемой, когда хотел сохранить даты в БД в формате UTC и избежать использования varchar
и явный String <-> java.util.Date
преобразований или установки всего приложения Java в часовом поясе UTC (потому что это может привести к другим неожиданным проблемам, если JVM будет использоваться во многих приложениях).
Итак, есть проект с открытым исходным кодом DbAssist
, что позволяет легко исправить чтение / запись как дату UTC из базы данных. Поскольку вы используете аннотации JPA для сопоставления полей в объекте, все, что вам нужно сделать, это включить следующую зависимость в свой Maven pom
файл:
<dependency>
<groupId>com.montrosesoftware</groupId>
<artifactId>DbAssist-5.2.2</artifactId>
<version>1.0-RELEASE</version>
</dependency>
Затем вы применяете исправление (для примера Hibernate + Spring Boot), добавляя @EnableAutoConfiguration
аннотация перед классом приложения Spring. Другие инструкции по установке и другие примеры использования можно найти в github проекта.
Хорошо, что вам совсем не нужно изменять сущности; вы можете оставить их java.util.Date
поля, как они есть.
5.2.2
должна соответствовать используемой вами версии Hibernate. Я не уверен, какую версию вы используете в своем проекте, но полный список предоставленных исправлений доступен на вики-странице проекта github. Причина, по которой исправление отличается для разных версий Hibernate, заключается в том, что создатели Hibernate несколько раз меняли API между выпусками.
Внутренне исправление использует подсказки от divestoclimb, Shane и нескольких других источников для создания пользовательского UtcDateType
, Затем он отображает стандарт java.util.Date
с обычаем UtcDateType
который обрабатывает всю необходимую обработку часового пояса. Отображение типов достигается с помощью @Typedef
аннотация в предоставленной package-info.java
файл.
@TypeDef(name = "UtcDateType", defaultForType = Date.class, typeClass = UtcDateType.class),
package com.montrosesoftware.dbassist.types;
Вы можете найти статью здесь, которая объясняет, почему такой временной сдвиг вообще происходит и каковы подходы к его решению.