Jak przechowywać datę / godzinę i znaczniki czasu w strefie czasowej UTC za pomocą JPA i Hibernate


106

Jak mogę skonfigurować JPA / Hibernate, aby przechowywać datę / godzinę w bazie danych jako strefę czasową UTC (GMT)? Rozważ tę jednostkę JPA z adnotacjami:

public class Event {
    @Id
    public int id;

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

Jeśli data to 2008-luty-03 9:30 czasu pacyficznego (PST), to chcę, aby w bazie danych przechowywany był czas UTC z okresu 2008-luty-03 17:30. Podobnie, gdy data jest pobierana z bazy danych, chcę, aby była interpretowana jako UTC. W tym przypadku 17:30 to 17:30 UTC. Po wyświetleniu będzie sformatowany jako 9:30 czasu PST.


1
Odpowiedź Vlada Mihalcei zapewnia zaktualizowaną odpowiedź (dla Hibernate
— 5.2+

Odpowiedzi:


73

Dzięki Hibernate 5.2 możesz teraz wymusić strefę czasową UTC, korzystając z następującej właściwości konfiguracyjnej:

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

Więcej informacji znajdziesz w tym artykule .


19
To też napisałem: D Zgadnij, kto dodał obsługę tej funkcji w Hibernate?
— Vlad Mihalcea

Och, właśnie teraz zdałem sobie sprawę, że nazwa i zdjęcie są takie same w twoim profilu i tych artykułach ... Dobra robota Vlad :)
— Dinei

@VladMihalcea jeśli jest przeznaczony dla Mysql, należy powiedzieć MySql, aby używał strefy czasowej, używając useTimezone=truew parametrach połączenia. Wtedy hibernate.jdbc.time_zonezadziała tylko ustawienie właściwości
— TheCoder

Właściwie musisz ustawić useLegacyDatetimeCodena false
— Vlad Mihalcea

2
hibernate.jdbc.time_zone wydaje się być ignorowane lub nie działa, gdy jest używany z PostgreSQL
— Alex R

48

Zgodnie z moją najlepszą wiedzą, musisz umieścić całą aplikację Java w strefie czasowej UTC (tak, aby Hibernate przechowywał daty w UTC) i będziesz musiał przekonwertować na dowolną żądaną strefę czasową podczas wyświetlania rzeczy (przynajmniej robimy to tą drogą).

Przy starcie wykonujemy:

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

I ustaw żądaną strefę czasową na DateFormat:

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

5
mitchnull, Twoje rozwiązanie nie będzie działać we wszystkich przypadkach, ponieważ Hibernate deleguje daty do sterownika JDBC, a każdy sterownik JDBC inaczej obsługuje daty i strefy czasowe. zobacz stackoverflow.com/questions/4123534/… .
— Derek Mahar

2
Ale jeśli uruchomię aplikację informującą o właściwości JVM „-Duser.timezone = + 00: 00”, to czy to samo się nie zachowa?
— rafa.ferreira

8
O ile wiem, będzie działać we wszystkich przypadkach, z wyjątkiem sytuacji, gdy JVM i serwer bazy danych znajdują się w różnych strefach czasowych.
— Shane

stevekuo i @mitchnull zobacz rozwiązanie divestoclimb poniżej, które jest znacznie lepsze i odporne na skutki uboczne stackoverflow.com/a/3430957/233906
— Cerber

Czy HibernateJPA obsługuje adnotacje „@Factory” i „@Externalizer”, tak robię obsługę datetime utc w bibliotece OpenJPA. stackoverflow.com/questions/10819862/…
— Kogo

44

Hibernate nie zna stref czasowych w datach (ponieważ ich nie ma), ale w rzeczywistości to warstwa JDBC powoduje problemy. ResultSet.getTimestampi PreparedStatement.setTimestampobaj twierdzą w swoich dokumentach, że domyślnie przekształcają daty do / z bieżącej strefy czasowej JVM podczas odczytu i zapisu z / do bazy danych.

Wymyśliłem rozwiązanie tego problemu w Hibernate 3.5 przez podklasę, org.hibernate.type.TimestampTypektóra zmusza te metody JDBC do używania UTC zamiast lokalnej strefy czasowej:

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

To samo należy zrobić, aby naprawić TimeType i DateType, jeśli używasz tych typów. Wadą jest to, że będziesz musiał ręcznie określić, że te typy mają być używane zamiast wartości domyślnych w każdym polu daty w twoich POJO (a także łamie czystą kompatybilność JPA), chyba że ktoś zna bardziej ogólną metodę nadpisywania.

AKTUALIZACJA: Hibernate 3.6 zmienił API typów. W 3.6 napisałem klasę UtcTimestampTypeDescriptor, aby to zaimplementować.

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

Teraz, gdy aplikacja zostanie uruchomiona, jeśli ustawisz TimestampTypeDescriptor.INSTANCE na wystąpienie UtcTimestampTypeDescriptor, wszystkie sygnatury czasowe będą przechowywane i traktowane jako podane w czasie UTC bez konieczności zmiany adnotacji w POJO. [Jeszcze tego nie testowałem]


3
Jak powiedzieć Hibernate, aby używał Twojego niestandardowego UtcTimestampType?
— Derek Mahar

divestoclimb, z którą wersją Hibernate jest UtcTimestampTypekompatybilna?
— Derek Mahar

2
„ResultSet.getTimestamp i PreparedStatement.setTimestamp podają w swoich dokumentach, że domyślnie przekształcają daty do / z bieżącej strefy czasowej JVM podczas odczytu i zapisu z / do bazy danych.” Czy masz referencje? Nie widzę żadnej wzmianki o tym w Javadocs Java 6 dla tych metod. Według stackoverflow.com/questions/4123534/… , w jaki sposób te metody stosują strefę czasową do danej Datelub Timestampzależy od sterownika JDBC.
— Derek Mahar

1
Mógłbym przysiąc, że czytałem o używaniu strefy czasowej JVM w zeszłym roku, ale teraz nie mogę jej znaleźć. Mogłem znaleźć to w dokumentacji dla konkretnego sterownika JDBC i uogólnić.
— divestoclimb

2
Aby wersja 3.6 twojego przykładu działała, musiałem utworzyć nowy typ, który był w zasadzie opakowaniem wokół TimeStampType, a następnie ustawić ten typ w polu.
— Shaun Stone,

17

Dzięki Spring Boot JPA użyj poniższego kodu w pliku application.properties i oczywiście możesz zmodyfikować strefę czasową według własnego uznania

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

Następnie w pliku klasy Entity

@Column
private LocalDateTime created;

11

Dodanie odpowiedzi, która jest całkowicie oparta na podpowiedziach Shauna Stone'a i zobowiązała się do jej pozbycia się. Chciałem tylko szczegółowo to przeliterować, ponieważ jest to częsty problem, a rozwiązanie jest nieco zagmatwane.

To używa Hibernate 4.1.4.Final, chociaż podejrzewam, że wszystko po 3.6 będzie działać.

Najpierw utwórz obiekt 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 );
            }
        };
    }
}

Następnie utwórz UtcTimestampType, który używa UtcTimestampTypeDescriptor zamiast TimestampTypeDescriptor jako SqlTypeDescriptor w wywołaniu super konstruktora, ale w przeciwnym razie deleguje wszystko do 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);
    }
}

Wreszcie, po zainicjowaniu konfiguracji Hibernacji, zarejestruj UtcTimestampType jako zastąpienie typu:

configuration.registerTypeOverride(new UtcTimestampType());

Teraz sygnatury czasowe nie powinny zajmować się strefą czasową maszyny JVM w drodze do iz bazy danych. HTH.


6
Byłoby wspaniale zobaczyć rozwiązanie dla JPA i Spring Configuration.
— Aubergine

2
Uwaga dotycząca stosowania tego podejścia w przypadku zapytań natywnych w Hibernate. Aby użyć tych przesłoniętych typów, należy ustawić wartość za pomocą query.setParameter (int pos, Object value), a nie query.setParameter (int pos, Date value, TemporalType temporalType). Jeśli użyjesz tego drugiego, Hibernate użyje swoich oryginalnych implementacji typu, ponieważ są one zakodowane na stałe.
— Nigel,

Gdzie powinienem wywołać instrukcję configuration.registerTypeOverride (new UtcTimestampType ()); ?
— Stony

@Stony Gdziekolwiek zainicjujesz konfigurację Hibernacji. Jeśli masz HibernateUtil (większość z nich), będzie tam.
— Shane,

1
Działa, ale po sprawdzeniu tego zdałem sobie sprawę, że nie jest to wymagane dla serwera postgres działającego w strefie czasowej = "UTC" i ze wszystkimi domyślnymi typami znaczników czasu jako "znacznik czasu ze strefą czasową" (robi się to wtedy automatycznie). Ale tutaj jest poprawiona wersja dla Hibernate 4.3.5 GA jako kompletna pojedyncza klasa i overridding spring factory bean pastebin.com/tT4ACXn6
— Łukasz Frankowski

10

Można by pomyśleć, że Hibernate rozwiązuje ten powszechny problem. Ale nie jest! Jest kilka „hacków”, aby to naprawić.

Jedynym, którego używam, jest przechowywanie daty jako długiej w bazie danych. Więc zawsze pracuję z milisekundami po 1/1/70. Mam wtedy metody pobierające i ustawiające w mojej klasie, które zwracają / akceptują tylko daty. Więc API pozostaje takie samo. Wadą jest to, że mam w bazie danych tęsknoty. Tak więc z SQL mogę robić tylko porównania <,>, = - nie wyszukane operatory daty.

Innym podejściem jest użycie niestandardowego typu mapowania, jak opisano tutaj: http://www.hibernate.org/100.html

Myślę, że właściwym sposobem radzenia sobie z tym jest jednak użycie kalendarza zamiast daty. Za pomocą kalendarza możesz ustawić TimeZone przed utrwaleniem.

UWAGA: Głupiutki stackoverflow nie pozwala mi komentować, więc oto odpowiedź na david a.

Jeśli utworzysz ten obiekt w Chicago:

new Date(0);

Hibernate utrzymuje go jako „12/31/1969 18:00:00”. Daty powinny być pozbawione strefy czasowej, więc nie jestem pewien, dlaczego miałaby zostać wprowadzona korekta.


1
Wstyd mi! Miałeś rację, a link w Twoim poście dobrze to wyjaśnia. Teraz chyba moja odpowiedź zasługuje na negatywną opinię :)
— david a.

1
Ani trochę. zachęciłeś mnie do opublikowania bardzo wyraźnego przykładu, dlaczego jest to problem.
— codefinger

4
Udało mi się poprawnie utrwalić czasy przy użyciu obiektu Calendar, dzięki czemu są one przechowywane w bazie danych jako UTC, jak sugerowałeś. Jednak podczas odczytywania utrwalonych jednostek z bazy danych Hibernate przyjmuje, że znajdują się one w lokalnej strefie czasowej, a obiekt Calendar jest nieprawidłowy!
— John K

1
John K, aby rozwiązać ten Calendarproblem z czytaniem, myślę, że Hibernate lub JPA powinny zapewnić jakiś sposób określenia dla każdego mapowania strefy czasowej, na którą Hibernate powinien przetłumaczyć datę, którą odczytuje i zapisuje w TIMESTAMPkolumnie.
— Derek Mahar

joekutner, po przeczytaniu stackoverflow.com/questions/4123534/… , przyszedłem podzielić się twoją opinią, że powinniśmy przechowywać milisekundy od Epoki w bazie danych, a nie a, Timestampponieważ nie możemy koniecznie ufać sterownikowi JDBC do przechowywania dat jako oczekiwalibyśmy.
— Derek Mahar

8

Działa tu kilka stref czasowych:

  1. Klasy Date w Javie (util i sql), które mają niejawne strefy czasowe UTC
  2. Strefa czasowa, w której działa maszyna JVM, i
  3. domyślna strefa czasowa serwera bazy danych.

Wszystkie te mogą być różne. Hibernate / JPA ma poważną wadę projektową polegającą na tym, że użytkownik nie może łatwo zapewnić, że informacje o strefie czasowej są przechowywane na serwerze bazy danych (co pozwala na odtworzenie poprawnych czasów i dat w JVM).

Bez możliwości (łatwego) przechowywania strefy czasowej za pomocą JPA / Hibernate, informacje są tracone, a po utracie ich konstruowanie staje się kosztowne (jeśli w ogóle możliwe).

Twierdziłbym, że lepiej jest zawsze przechowywać informacje o strefie czasowej (powinno być to ustawienie domyślne), a użytkownicy powinni mieć wtedy opcjonalną możliwość optymalizacji strefy czasowej (chociaż tak naprawdę wpływa to tylko na wyświetlanie, nadal istnieje domniemana strefa czasowa w dowolnej dacie).

Przepraszamy, ten post nie zapewnia obejścia (odpowiedź została udzielona w innym miejscu), ale jest to uzasadnienie, dlaczego zawsze ważne jest przechowywanie informacji o strefie czasowej. Niestety, wydaje się, że wielu informatyków i praktyków programowania argumentuje przeciwko potrzebie stref czasowych po prostu dlatego, że nie doceniają perspektywy "utraty informacji" i tego, jak bardzo utrudnia to takie rzeczy jak internacjonalizacja - co jest bardzo ważne w dzisiejszych czasach, gdy strony internetowe są dostępne przez klientów i ludzi w Twojej organizacji, gdy przemieszczają się po świecie.


1
„Hibernate / JPA ma poważną wadę projektową”, powiedziałbym, że jest to niedostatek języka SQL, który tradycyjnie pozwalał na utajnienie strefy czasowej, a zatem potencjalnie na wszystko. Głupi SQL.
— Raedwald

3
Właściwie zamiast zawsze zapisywać strefę czasową, możesz również ustandaryzować jedną strefę czasową (zwykle UTC) i przekonwertować wszystko do tej strefy czasowej, gdy się utrzymuje (i z powrotem podczas czytania). To jest to, co zwykle robimy. Jednak JDBC nie obsługuje tego bezpośrednio: - /.
— sleske,

3

Proszę spojrzeć na mój projekt na Sourceforge, który zawiera typy użytkowników dla standardowych typów daty i czasu SQL, a także JSR 310 i Joda Time. Wszystkie typy próbują rozwiązać problem kompensacji. Zobacz http://sourceforge.net/projects/usertype/

EDYCJA: W odpowiedzi na pytanie Dereka Mahara dołączone do tego komentarza:

„Chris, czy twoje typy użytkowników działają z Hibernate 3 lub nowszym? - Derek Mahar, 7 listopada 2010 o 12:30”

Tak, te typy obsługują wersje Hibernate 3.x, w tym Hibernate 3.6.


2

Data nie znajduje się w żadnej strefie czasowej (jest to urząd milisekundowy od określonego momentu w tym samym czasie dla wszystkich), ale bazowe (R) bazy danych zazwyczaj przechowują znaczniki czasu w formacie politycznym (rok, miesiąc, dzień, godzina, minuta, sekunda,. ..), która jest wrażliwa na strefę czasową.

Aby być poważnym, Hibernacja MUSI pozwolić, aby w ramach jakiejś formy mapowania została poinformowana, że ​​data DB jest w takiej a takiej strefie czasowej, aby podczas ładowania lub przechowywania nie przyjmowała własnej ...


1

Napotkałem ten sam problem, gdy chciałem przechowywać daty w bazie danych jako UTC i unikać używania varchari jawnych String <-> java.util.Datekonwersji lub ustawiania całej mojej aplikacji Java w strefie czasowej UTC (ponieważ może to prowadzić do innych nieoczekiwanych problemów, jeśli JVM jest współdzielone w wielu aplikacjach).

Tak więc istnieje projekt open source DbAssist, który pozwala łatwo naprawić odczyt / zapis jako datę UTC z bazy danych. Ponieważ używasz adnotacji JPA do mapowania pól w encji, wszystko, co musisz zrobić, to uwzględnić następującą zależność w pompliku Maven :

<dependency>
    <groupId>com.montrosesoftware</groupId>
    <artifactId>DbAssist-5.2.2</artifactId>
    <version>1.0-RELEASE</version>
</dependency>

Następnie zastosuj poprawkę (dla przykładu Hibernate + Spring Boot), dodając @EnableAutoConfigurationadnotację przed klasą aplikacji Spring. Aby uzyskać instrukcje instalacji innych ustawień i więcej przykładów użycia, po prostu zapoznaj się z githubem projektu .

Dobrą rzeczą jest to, że nie musisz w ogóle modyfikować jednostek; możesz zostawić ich java.util.Datepola takimi, jakimi są.

5.2.2musi odpowiadać używanej wersji Hibernacji. Nie jestem pewien, której wersji używasz w swoim projekcie, ale pełna lista dostarczonych poprawek jest dostępna na stronie wiki github projektu . Powodem, dla którego poprawka jest inna dla różnych wersji Hibernate, jest to, że twórcy Hibernate zmieniali API kilka razy między wydaniami.

Wewnętrznie, poprawka wykorzystuje wskazówki od divestoclimb, Shane i kilku innych źródeł w celu utworzenia niestandardowego UtcDateType. Następnie mapuje standard java.util.Datez niestandardowym, UtcDateTypektóry obsługuje całą niezbędną obsługę stref czasowych. Mapowanie typów uzyskuje się za pomocą @Typedefadnotacji w dostarczonym package-info.javapliku.

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

Możesz znaleźć tutaj artykuł , który wyjaśnia, dlaczego takie przesunięcie czasu w ogóle zachodzi i jakie są sposoby jego rozwiązania.


1

Hibernate nie pozwala na określanie stref czasowych za pomocą adnotacji ani w żaden inny sposób. Jeśli zamiast daty używasz Kalendarza, możesz zaimplementować obejście problemu przy użyciu właściwości AccessType HIbernate i samodzielnie zaimplementować mapowanie. Bardziej zaawansowanym rozwiązaniem jest zaimplementowanie niestandardowego typu użytkownika w celu zmapowania daty lub kalendarza. Oba rozwiązania są wyjaśnione w moim blogu tutaj: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.