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.

Steve Kuo
źródło
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 .

Vlad Mihalcea
źródło
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"))
mitchnull
źródło
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]

divestoclimb
źródło
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;
JavaGeek
źródło
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.

Shane
źródło
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.

codefinger
źródło
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.

Moa
źródło
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.

Chris Pheby
źródło
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 ...


źródło
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.

orim
źródło
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

joobik
źródło