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

106

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

public class Event {
    @Id
    public int id;

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

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

Стив Куо
источник
1
Ответ Влада Михалчи предоставляет обновленный ответ (для Hibernate 5.2+)
Dinei

Ответы:

73

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

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

Подробнее читайте в этой статье .

Влад Михалча
источник
19
Я тоже это написал: D А теперь угадайте, кто добавил поддержку этой функции в Hibernate?
Влад Михалча
О, только сейчас я понял, что имя и изображение совпадают в твоем профиле и в тех статьях ... Молодец, Влад :)
Диней
@VladMihalcea, если это для Mysql, нужно указать MySql использовать часовой пояс, используя его useTimezone=trueв строке подключения. Тогда hibernate.jdbc.time_zoneбудет работать только свойство настройки
TheCoder
На самом деле нужно установить useLegacyDatetimeCodefalse
Влад Михалча
2
hibernate.jdbc.time_zone игнорируется или не действует при использовании с PostgreSQL
Alex R
48

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

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

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

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

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
Mitchnull
источник
5
mitchnull, ваше решение не будет работать во всех случаях, потому что Hibernate делегирует установку дат драйверу JDBC, а каждый драйвер JDBC обрабатывает даты и часовые пояса по-разному. см. stackoverflow.com/questions/4123534/… .
Дерек Махар
2
Но если я запускаю свое приложение, информирующее свойство JVM "-Duser.timezone = + 00: 00", разве это не то же самое?
rafa.ferreira
8
Насколько я могу судить, он будет работать во всех случаях, кроме случаев, когда JVM и сервер базы данных находятся в разных часовых поясах.
Шейн
stevekuo и @mitchnull см. ниже решение divestoclimb, которое намного лучше и доказывает побочные эффекты stackoverflow.com/a/3430957/233906
Cerber
Поддерживает ли HibernateJPA аннотации «@Factory» и «@Externalizer», это то, как я выполняю обработку utc datetime в библиотеке OpenJPA. stackoverflow.com/questions/10819862/…
Whome
44

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, если вы используете эти типы. Обратной стороной является то, что вам придется вручную указать, что эти типы должны использоваться вместо значений по умолчанию в каждом поле даты в ваших 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. [Я еще не тестировал это]

лишить конечности
источник
3
Как вы укажете Hibernate, что нужно использовать ваш собственный UtcTimestampType?
Дерек Махар
divestoclimb, с какой версией Hibernate UtcTimestampTypeсовместима?
Дерек Махар
2
«ResultSet.getTimestamp и PreparedStatement.setTimestamp оба говорят в своих документах, что они по умолчанию преобразуют даты в / из текущего часового пояса JVM при чтении и записи из / в базу данных». У вас есть ссылка? Я не вижу упоминания об этом в документации Java 6 для этих методов. Согласно stackoverflow.com/questions/4123534/… , как эти методы применяют часовой пояс к заданному Dateили Timestampзависит от драйвера JDBC.
Дерек Махар
1
Я мог бы поклясться, что читал об использовании часового пояса JVM в прошлом году, но теперь я не могу его найти. Я мог бы найти его в документации для конкретного драйвера JDBC и обобщить.
divestoclimb
2
Чтобы заставить работать версию 3.6 вашего примера, мне пришлось создать новый тип, который был в основном оберткой вокруг TimeStampType, а затем установить этот тип в поле.
Shaun Stone
17

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

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

Затем в вашем файле класса Entity

@Column
private LocalDateTime created;
JavaGeek
источник
11

Добавление ответа, полностью основанного на 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 на пути к базе данных и из нее. HTH.

Шейн
источник
6
Было бы здорово увидеть решение для JPA и Spring Configuration.
Баклажан
2
Замечание об использовании этого подхода с собственными запросами в Hibernate. Чтобы использовать эти переопределенные типы, вы должны установить значение с помощью query.setParameter (int pos, Object value), а не query.setParameter (int pos, Date value, TemporalType temporalType). Если вы используете последний, то Hibernate будет использовать свои исходные реализации типов, поскольку они жестко запрограммированы.
Найджел
Где я должен вызвать конфигурацию оператора .registerTypeOverride (new UtcTimestampType ()); ?
Stony
@Stony Где бы вы ни инициализировали конфигурацию Hibernate. Если у вас есть HibernateUtil (у большинства из них), он будет там.
Шейн
1
Это работает, но после проверки я понял, что это не требуется для сервера postgres, работающего в timezone = "UTC" и со всеми типами временных меток по умолчанию как "timestamp with time zone" (тогда это делается автоматически). Но вот фиксированная версия для Hibernate 4.3.5 GA в виде полного одиночного класса и замещающего компонента Spring
Лукаш Франковски
10

Можно подумать, что эту распространенную проблему решит Hibernate. Но это не так! Есть несколько "приемов", чтобы исправить это.

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

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

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

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

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

new Date(0);

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

кодовый палец
источник
1
Мне стыдно! Вы были правы, и ссылка из вашего сообщения хорошо это объясняет. Теперь я полагаю, что мой ответ заслуживает некоторой отрицательной репутации :)
david a.
1
Не за что. вы побудили меня опубликовать очень явный пример того, почему это проблема.
codefinger,
4
Я смог правильно сохранить время, используя объект Calendar, чтобы они хранились в БД как UTC, как вы предложили. Однако при чтении сохраненных сущностей обратно из базы данных Hibernate предполагает, что они находятся в местном часовом поясе, а объект Calendar неверен!
Джон К.
1
Джон К., чтобы решить эту Calendarпроблему чтения, я думаю, что Hibernate или JPA должны предоставить какой-то способ указать для каждого сопоставления часовой пояс, в который Hibernate должен переводить дату, которую он читает и записывает в TIMESTAMPстолбец.
Дерек Махар
joekutner, после прочтения stackoverflow.com/questions/4123534/… , я пришел к тому, чтобы поделиться вашим мнением о том, что мы должны хранить миллисекунды с эпохи в базе данных, а не в, Timestampпоскольку мы не можем обязательно доверять драйверу JDBC для хранения дат как мы ожидали.
Дерек Махар
8

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

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

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

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

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

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

Моа
источник
1
«Hibernate / JPA имеет серьезный недостаток дизайна». Я бы сказал, что это недостаток в SQL, который традиционно позволял неявно указывать часовой пояс и, следовательно, потенциально что угодно. Глупый SQL.
Raedwald
3
Фактически, вместо того, чтобы всегда сохранять часовой пояс, вы также можете стандартизировать один часовой пояс (обычно UTC) и преобразовывать все в этот часовой пояс при сохранении (и обратно при чтении). Это то, что мы обычно делаем. Однако JDBC не поддерживает это напрямую: - /.
sleske
3

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

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

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

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

Крис Феби
источник
2

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

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


источник
1

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

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

<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;

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

Орим
источник
1

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

джубик
источник