Як зберігати дату / час та часові позначки в часовому поясі UTC за допомогою JPA та Hibernate


106

Як можна налаштувати 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 17:30 зберігався в базі даних. Так само, коли дата витягується з бази даних, я хочу, щоб її інтерпретували як UTC. Так що в цьому випадку 530pm - 530pm UTC. Коли він відображатиметься, він буде відформатований як 9:30 ранку PST.


1
Відповідь Влада Міхалчея дає оновлену відповідь (для сплячого режиму 5.2+)
Діней

Відповіді:


73

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

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

Більш детально ознайомтеся з цією статтею .


19
Я теж це написав: D Тепер здогадайтесь, хто додав підтримку цієї функції в режимі сплячки?
Влад Михальча

О, тільки що я зрозумів, що ім’я та зображення однакові у вашому профілі та тих статтях ... Гарна робота Влад :)
Діней

@VladMihalcea, якщо це для Mysql, потрібно сказати MySql використовувати часовий пояс, використовуючи useTimezone=trueв рядку з'єднання. Тоді лише налаштування власності hibernate.jdbc.time_zoneбуде працювати
TheCoder

Власне, вам потрібно встановити useLegacyDatetimeCodeзначення false
Влад Михалча

2
hibernate.jdbc.time_zone, імовірно, ігнорується або не має ефекту при використанні з PostgreSQL
Alex R

48

Наскільки мені відомо, вам потрібно помістити весь додаток Java у часовий пояс UTC (щоб сплячий час зберігав дати в UTC), і вам потрібно буде перетворити на потрібний часовий пояс під час відображення матеріалів (принаймні, ми це робимо сюди).

При запуску ми робимо:

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

І встановіть бажаний часовий пояс у DateFormat:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))

5
mitchnull, ваше рішення працюватиме не у всіх випадках, оскільки сплячий делегує, встановлюючи дати драйверу 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

Зимова сплячка не знає про часові пояси в датах (оскільки таких немає), але насправді шар 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. [Я цього ще не перевіряв]


3
Як ви можете сказати сплячому користуватися своїм звичаєм UtcTimestampType?
Дерек Махар

divestoclimb, з якою версією Hibernate UtcTimestampTypeсумісна версія ?
Дерек Махар

2
"Обидва ResultSet.getTimestamp та PreparedStatement.setTimestamp в своїх документах кажуть, що вони перетворюють дати в / з поточного часового поясу JVM за замовчуванням під час читання та запису з / у базу даних." У вас є довідник? Я не бачу жодної згадки про це в Java 6 Javadocs для цих методів. Відповідно до stackoverflow.com/questions/4123534/… , як ці методи застосовують часовий пояс до заданого Dateабо Timestampзалежать від драйвера JDBC.
Дерек Махар

1
Я міг би скласти присягу, що прочитав про використання часового поясу JVM минулого року, але зараз не можу його знайти. Я, можливо, знайшов його в документах для конкретного драйвера JDBC та узагальнив.
divestoclimb

2
Щоб 3.6 версія вашого прикладу працювала, мені довелося створити новий тип, який в основному був обгорткою навколо TimeStampType, а потім встановив цей тип у полі.
Шон Стоун

17

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

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

Потім у вашому файлі класу Entity,

@Column
private LocalDateTime created;

11

Додавши відповідь, повністю заснована на та заборгованій заглиблюватися на підйомі з натяком на Шон Стоун. Просто хотілося детально прописати це, оскільки це звичайна проблема, і рішення трохи заплутане.

Для цього використовується 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.
Баклажан

2
Примітка щодо використання цього підходу з нативними запитами в сплячому режимі. Для використання цих перекритих типів потрібно встановити значення за допомогою query.setParameter (int pos, значення об'єкта), а не query.setParameter (int pos, значення дати, temporalType temporalType). Якщо ви використовуєте останнє, Hibernate буде використовувати свої оригінальні типові реалізації, оскільки вони жорстко закодовані.
Найджел

Де я повинен викликати конфігурацію оператора.registerTypeOverride (новий UtcTimestampType ()); ?
Кам’яний

@Stony Де б ви не ініціалізували свою конфігурацію зі сну. Якщо у вас є HibernateUtil (більшість з них), він буде там.
Шейн

1
Це працює, але перевіривши його, я зрозумів, що він не потрібен серверам Postgres, що працюють в timezone = "UTC", а для всіх типів часових міток за замовчуванням як "timetamp with time zone" (це робиться автоматично). Але, ось виправлена ​​версія для Hibernate 4.3.5 GA як повноцінного однокласного і переважаючого весняних заводських бобів pastebin.com/tT4ACXn6
Лукаш Франковський

10

Ви можете подумати, що цією проблемою опікується сплячка. Але це не так! Існує кілька "хаків", щоб правильно це зробити.

Я використовую, щоб зберігати Дату як Довгу у базі даних. Тому я завжди працюю з мілісекундами після 1/1/70. Тоді в моєму класі є жителі та сетери, які повертають / приймають лише дати. Отже API залишається тим самим. Нижня сторона полягає в тому, що я маю довго в базі даних. Так що з SQL я можу зробити лише порівняння <,>, = порівняння - не фантазійні оператори дат.

Інший підхід полягає у використанні користувальницького типу відображення, як описано тут: http://www.hibernate.org/100.html

Я думаю, що правильний спосіб вирішити це - використовувати Календар замість дати. За допомогою календаря ви можете встановити TimeZone перед збереженням.

ПРИМІТКА: Дурний стаковерх не дасть мені коментарів, тому ось відповідь на David a.

Якщо ви створили цей об’єкт у Чикаго:

new Date(0);

Зимовий режим зберігає це як "31.12.1969 18:00:00". Дати повинні бути позбавлені часового поясу, тому я не впевнений, чому було б внесено коригування.


1
Мені так соромно! Ви мали рацію, і посилання з вашого допису це добре пояснює. Зараз я думаю, що моя відповідь заслуговує на деяку негативну репутацію :)
david a.

1
Зовсім ні. ви заохочували мене викладати дуже чіткий приклад, чому це проблема.
codefinger

4
Мені вдалося правильно витримати час, використовуючи об’єкт Calendar, щоб вони зберігалися в БД як UTC, як ви запропонували. Однак, читаючи збережені об’єкти із бази даних, Спящий припускає, що вони перебувають у локальному часовому поясі, а об’єкт Календар неправильний!
Джон К

1
Джон К, для того, щоб вирішити цю Calendarпроблему з читанням, я думаю, що сплячий або JPA повинен передбачити певний спосіб вказати для кожного відображення часовий пояс, до якого сплячий повинен перевести дату, яку він читає, і записує у TIMESTAMPстовпчик.
Дерек Махар

joekutner, після прочитання stackoverflow.com/questions/4123534 / ... , я прийшов , щоб висловити свою думку , що ми повинні зберігати мілісекунди з епохи в базі даних , а не Timestampтак як ми не можемо обов'язково довіряти драйвер JDBC для зберігання дат , як ми б очікували
Дерек Махар

8

Тут працює кілька часових поясів:

  1. Класи дати Java (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/

EDIT: У відповідь на питання Дерека Махара, що додається до цього коментаря:

"Кріс, чи працюють твої користувачі зі сплячим режимом 3 або вище? - Дерек Махар 7 листопада '10 о 12:30"

Так, ці типи підтримують Hibernate 3.x версії, включаючи Hibernate 3.6.


2

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

Якщо бути серйозним, Зимуваному ПОВИННІ бути дозволено сказати в рамках якоїсь форми відображення, що дата БД знаходиться в такому часовому поясі, щоб, коли він завантажує або зберігає, він не передбачає свого ...


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має відповідати гібернаційній версії, яку ви використовуєте. Я не впевнений, яку версію ви використовуєте у своєму проекті, але повний перелік наданих виправлень доступний на вікі-сторінці 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 не дозволяє вказувати часові пояси анотацією чи будь-якими іншими способами. Якщо ви використовуєте Календар замість дати, ви можете реалізувати рішення, використовуючи властивість HIbernate AccessType та реалізуючи відображення самостійно. Більш досконалим рішенням є впровадження користувальницького типу UserType для відображення дати або календаря. Обидва рішення пояснюються в моєму дописі в блозі тут: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.