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


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

public class Event {
    @Id
    public int id;

    @Temporal(TemporalType.TIMESTAMP)
    public java.util.Date date;
}

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

10 81

10 ответов:

С помощью 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 не знает о часовых поясах в датах (потому что их нет), но на самом деле это слой 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, если вы используете эти типы. Недостатком является то, что вам придется вручную указать, что эти типы должны использоваться вместо значений по умолчанию для каждого поля даты в вашем POJOs (а также нарушает чистую совместимость 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.Экземпляр к экземпляру UtcTimestampTypeDescriptor, все метки времени будут сохранены и обработаны как находящиеся в UTC без изменения аннотаций на POJOs. [Я еще не проверял это]

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

это использование Hibernate 4.1.4.Финал, хотя я подозреваю, что после 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 <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/1/70. Затем у меня есть геттеры и сеттеры в моем классе, которые возвращают/принимают только даты. Таким образом, API остается прежним. Недостатком является то, что у меня есть лонги в базе данных. Так что с SQL я могу в значительной степени только делать, = сравнения -- нет необычные операторы даты.

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

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

Примечание: глупый stackoverflow не позволит мне комментировать, так что вот ответ на david a.

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

new Date(0);

Hibernate сохраняет его как "12/31/1969 18: 00: 00". Даты должны быть лишены часового пояса, поэтому я не уверен, почему будет сделана корректировка.

здесь работает несколько часовых поясов:

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

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

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

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

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

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

EDIT: в ответ на вопрос Дерека Махара, приложенный к этому комментарию:

"Крис, ваши типы пользователей работают с Hibernate 3 или выше? - Дерек Махар 7 '10 ноября в 12: 30"

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

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

чтобы быть серьезным, спящий режим должны разрешите сообщить в какой-либо форме отображения, что дата БД находится в таком-то часовом поясе, так что при загрузке или хранении он не принимает свой собственный...

Hibernate не позволяет указывать часовые пояса аннотациями или любыми другими средствами. Если вы используете календарь вместо даты, вы можете реализовать обходной путь с помощью свойства HIbernate AccessType и реализовать сопоставление самостоятельно. Более продвинутым решением является реализация пользовательского типа пользователя для сопоставления даты или календаря. Оба решения объясняются в этом блоге: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 file:

<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, Шейн и несколько других источников для того, чтобы создать пользовательский UtcDateType. Затем он отображает стандарт java.util.Date С пользовательской UtcDateType который обрабатывает всю необходимую обработку часового пояса. Отображение типов достигается с помощью @Typedef аннотация в предоставленной .

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

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