Как хранить дату и время и временные метки в часовом поясе 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.
Ответы
Ответ 1
С Hibernate 5.2 вы можете принудительно установить часовой пояс UTC, используя следующее свойство конфигурации:
<property name="hibernate.jdbc.time_zone" value="UTC"/>
Для получения дополнительной информации ознакомьтесь с этой статьей.
Ответ 2
Насколько я знаю, вам нужно разместить все Java-приложение в часовой пояс UTC (чтобы Hibernate сохранил даты в UTC), и вам нужно будет конвертировать в любой часовой пояс, который требуется при отображении материала (по крайней мере, мы делаем это таким образом).
При запуске мы делаем:
TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
И установите желаемый часовой пояс для DateFormat:
fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
Ответ 3
Hibernate не знает о часовом поясе в Датах (потому что его нет), но на самом деле это слой 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), если только кто-то не знает более общий метод переопределения.
UPDATE: 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. [Я еще не тестировал это]
Ответ 4
Добавляем ответ, который полностью на основе и обязанность divestoclimb с подсказкой от Shaun Stone. Просто хотел подробно изложить это, поскольку это общая проблема, и решение немного запутанно.
Это использование Hibernate 4.1.4.Final, хотя я подозреваю, что после 3.6 будет работать.
Сначала создайте divestoclimb 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 по пути в базу данных и из нее. НТН.
Ответ 5
Вы могли бы подумать, что эта общая проблема будет решена с помощью Hibernate. Но его нет! Есть несколько "хаков", чтобы понять это правильно.
Тот, который я использую, заключается в том, чтобы хранить дату в формате Long в базе данных. Поэтому я всегда работаю с миллисекундами после 1/1/70. Затем у меня есть получатели и сеттеры на моем классе, которые возвращают/принимают только Даты. Таким образом, API остается прежним. Нижняя сторона заключается в том, что у меня есть длинные базы данных. SO с SQL я могу в значительной степени выполнять только <, > , = сравнения - не причудливые операторы даты.
Другим подходом является пользовательский тип пользовательского сопоставления, как описано здесь:
http://www.hibernate.org/100.html
Я думаю, что правильный способ справиться с этим - использовать Календарь вместо Даты. С помощью Calendar вы можете установить TimeZone перед сохранением.
ПРИМЕЧАНИЕ. Глупый stackoverflow не позволяет мне комментировать, так что вот ответ на david a.
Если вы создадите этот объект в Чикаго:
new Date(0);
Спящий режим сохраняет это как "31.12.1969 18:00:00". Даты должны быть лишены часовой пояс, поэтому я не уверен, почему будет выполнена настройка.
Ответ 6
Здесь есть несколько временных зон:
- Классы Java Date (util и sql), которые имеют скрытые временные интервалы
of UTC
- Часовой пояс, на котором работает JVM, и
- часовой пояс по умолчанию для вашего сервера базы данных.
Все они могут быть разными. Hibernate/JPA имеет серьезный недостаток в дизайне, поскольку пользователь не может легко обеспечить сохранение информации о часовом поясе на сервере базы данных (что позволяет восстановить правильные времена и даты в JVM).
Без возможности (легко) хранить часовой пояс с помощью JPA/Hibernate, информация теряется, и как только информация теряется, становится дорого ее строить (если это вообще возможно).
Я бы сказал, что лучше всегда хранить информацию о часовом поясе (должен быть по умолчанию), и тогда у пользователей должна быть дополнительная возможность оптимизировать часовой пояс (хотя это действительно влияет на отображение, по-прежнему существует неявный часовой пояс в любом дата).
Извините, этот пост не обеспечивает обход (который был отвечен в другом месте), но это рационализация, почему важно всегда хранить информацию о часовом поясе. К сожалению, многие компьютерные ученые и программисты не согласны с необходимостью делать часовые пояса просто потому, что они не понимают перспективы "потери информации" и как это очень затрудняет такие вещи, как интернационализация, что очень важно в наши дни с веб-сайтами, доступными клиентов и людей в вашей организации, когда они перемещаются по всему миру.
Ответ 7
Пожалуйста, посмотрите мой проект на Sourceforge, который имеет типы пользователей для стандартных типов даты и времени SQL, а также JSR 310 и Joda Time. Все типы пытаются решить проблему смещения. См. http://sourceforge.net/projects/usertype/
EDIT: В ответ на вопрос Дерека Махара прилагается к этому комментарию:
"Крис, ваши типы пользователей работают с Hibernate 3 или выше? - Derek Mahar 7 ноября в 12:30"
Да, эти типы поддерживают версии Hibernate 3.x, включая Hibernate 3.6.
Ответ 8
Дата не находится в каком-либо часовом поясе (это офис в миллисекундах с определенного момента времени, такого же для всех), но базовые (R) DB обычно хранят временные метки в политическом формате (год, месяц, день, час, минута, второй,...), который чувствителен к часовому поясу.
Чтобы быть серьезным, Hibernate ДОЛЖНО быть разрешено сообщать в какой-то форме сопоставления, что дата БД находится в таком-то ином часовом поясе, так что когда она загружает или хранит ее, она не принимает своих собственных...
Ответ 9
Спящий режим не позволяет указывать часовые пояса по аннотации или любым другим способом. Если вы используете календарь вместо даты, вы можете реализовать обходное решение с использованием свойства HIbernate AccessType и реализовать само отображение. Более продвинутое решение заключается в реализации пользовательского UserType для сопоставления даты или календаря. Оба решения объясняются в этом сообщении в блоге: http://dev-metal.blogspot.com/2010/11/mapping-dates-and-time-zones-with.html
Ответ 10
Я столкнулся с одной и той же проблемой, когда я хотел хранить даты в БД в формате UTC и избегать использования преобразований varchar
и явных String <-> java.util.Date
или установки всего приложения Java в часовой пояс UTC (поскольку это может привести к к другим непредвиденным проблемам, если JVM используется во многих приложениях).
Итак, есть проект с открытым исходным кодом DbAssist
, который позволяет вам легко исправить чтение/запись в качестве даты UTC из базы данных. Поскольку вы используете JPA Annotations для сопоставления полей в сущности, все, что вам нужно сделать, это включить следующую зависимость к вашему файлу 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;
Здесь вы можете найти статью которая объясняет, почему такой сдвиг во времени происходит вообще и каковы подходы к его решению.
Ответ 11
Используя Spring Boot JPA, используйте приведенный ниже код в файле application.properties, и, очевидно, вы можете изменить часовой пояс по своему выбору.
spring.jpa.properties.hibernate.jdbc.time_zone = UTC
Затем в вашем файле класса Entity,
@Column
private LocalDateTime created;