Как сохранить дату / время и временные метки в часовом поясе UTC с JPA и Hibernate - PullRequest
91 голосов
/ 03 февраля 2009

Как я могу настроить 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), то я хочу, чтобы в базе данных сохранялось время по Гринвичу, 2008 г., 3 февраля - 17:30. Аналогично, когда дата извлекается из базы данных, я хочу, чтобы она интерпретировалась как UTC. Таким образом, в этом случае 5:30 вечера - это 5:30 вечера UTC. Когда он отображается, он будет отформатирован как 9:30 утра по тихоокеанскому времени.

Ответы [ 11 ]

49 голосов
/ 03 февраля 2009

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

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

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

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

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
47 голосов
/ 05 ноября 2016

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

<property name="hibernate.jdbc.time_zone" value="UTC"/>

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

41 голосов
/ 07 августа 2010

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. [Я еще не проверял это]

11 голосов
/ 06 июня 2013

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

Это использует 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 <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 на пути к базе данных и из нее. НТН.

10 голосов
/ 17 февраля 2009

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

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

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

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

ПРИМЕЧАНИЕ. Глупый стековый поток не позволяет мне комментировать, поэтому вот ответ Дэвиду а.

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

new Date(0);

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

8 голосов
/ 14 декабря 2010

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

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

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

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

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

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

3 голосов
/ 27 марта 2019

Используя Spring Boot JPA, используйте приведенный ниже код в файле application.properties, и, очевидно, вы можете изменить часовой пояс по своему выбору

spring.jpa.properties.hibernate.jdbc.time_zone = UTC

Затем в вашем файле класса сущностей,

@Column
private LocalDateTime created;
3 голосов
/ 01 мая 2010

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

РЕДАКТИРОВАТЬ: В ответ на вопрос Дерека Махара, прикрепленный к этому комментарию:

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

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

2 голосов
/ 23 марта 2009

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

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

1 голос
/ 08 ноября 2016

Я столкнулся с той же проблемой, когда хотел сохранить даты в БД в формате 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;

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...