Speichern von Datum / Uhrzeit und Zeitstempeln in der UTC-Zeitzone mit JPA und Hibernate

106

Wie kann ich JPA / Hibernate so konfigurieren, dass Datum und Uhrzeit in der Datenbank als UTC-Zeitzone (GMT) gespeichert werden? Betrachten Sie diese kommentierte JPA-Entität:

public class Event {
    @Id
    public int id;

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

Wenn das Datum 2008-Feb-03 9:30 Uhr Pacific Standard Time (PST) ist, möchte ich, dass die UTC-Zeit von 2008-Feb-03 17:30 Uhr in der Datenbank gespeichert wird. Wenn das Datum aus der Datenbank abgerufen wird, möchte ich, dass es als UTC interpretiert wird. In diesem Fall ist 17:30 Uhr 17:30 Uhr UTC. Wenn es angezeigt wird, wird es als 9:30 Uhr PST formatiert.

Steve Kuo
quelle
1
Vlad Mihalcea Antwort liefert eine aktualisierte Antwort (für Hibernate 5.2+)
Dinei

Antworten:

73

Mit Hibernate 5.2 können Sie jetzt die UTC-Zeitzone mithilfe der folgenden Konfigurationseigenschaft erzwingen:

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

Weitere Informationen finden Sie in diesem Artikel .

Vlad Mihalcea
quelle
19
Ich habe auch das geschrieben: D Nun, raten Sie mal, wer diese Funktion in Hibernate unterstützt hat.
Vlad Mihalcea
Oh, gerade jetzt habe ich festgestellt, dass der Name und das Bild in Ihrem Profil und diesen Artikeln gleich sind ... Gute Arbeit Vlad :)
Dinei
@VladMihalcea Wenn es sich um MySQL handelt, muss MySql angewiesen werden, die Zeitzone mithilfe useTimezone=trueder Verbindungszeichenfolge zu verwenden. Dann hibernate.jdbc.time_zonefunktioniert nur die Einstellungseigenschaft
TheCoder
Eigentlich müssen Sie useLegacyDatetimeCodeauf false setzen
Vlad Mihalcea
2
hibernate.jdbc.time_zone scheint ignoriert zu werden oder hat keine Auswirkung, wenn es mit PostgreSQL verwendet wird
Alex R
48

Nach meinem besten Wissen müssen Sie Ihre gesamte Java-App in die UTC-Zeitzone stellen (damit Hibernate Daten in UTC speichert), und Sie müssen in die gewünschte Zeitzone konvertieren, wenn Sie Inhalte anzeigen (zumindest tun wir dies diesen Weg).

Beim Start machen wir:

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

Und stellen Sie die gewünschte Zeitzone auf das DateFormat ein:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
mitchnull
quelle
5
Mitchnull, Ihre Lösung funktioniert nicht in allen Fällen, da Hibernate-Delegierte Datumsangaben für den JDBC-Treiber festlegen und jeder JDBC-Treiber Datums- und Zeitzonen unterschiedlich behandelt. Siehe stackoverflow.com/questions/4123534/… .
Derek Mahar
2
Aber wenn ich meine App starte und über die JVM-Eigenschaft "-Duser.timezone = + 00: 00" informiere, verhält sich das nicht gleich?
rafa.ferreira
8
Soweit ich das beurteilen kann, funktioniert es in allen Fällen, außer wenn sich die JVM und der Datenbankserver in unterschiedlichen Zeitzonen befinden.
Shane
stevekuo und @mitchnull siehe divestoclimb Lösung unter denen ist viel besser und Nebenwirkung Beweis stackoverflow.com/a/3430957/233906
Cerber
Unterstützt HibernateJPA die Annotation "@Factory" und "@Externalizer"? So verarbeite ich datetime utc in der OpenJPA-Bibliothek. stackoverflow.com/questions/10819862/…
Wen am
44

Hibernate kennt keine Zeitzonen in Dates (weil es keine gibt), aber es ist tatsächlich die JDBC-Ebene, die Probleme verursacht. ResultSet.getTimestampund PreparedStatement.setTimestampbeide sagen in ihren Dokumenten, dass sie beim Lesen und Schreiben von / in die Datenbank standardmäßig Daten in / aus der aktuellen JVM-Zeitzone transformieren.

Ich habe in Hibernate 3.5 eine Lösung für dieses Problem gefunden, indem ich eine Unterklasse erstellt habe org.hibernate.type.TimestampType, die diese JDBC-Methoden zwingt, UTC anstelle der lokalen Zeitzone zu verwenden:

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

Dasselbe sollte getan werden, um TimeType und DateType zu reparieren, wenn Sie diese Typen verwenden. Der Nachteil ist, dass Sie manuell angeben müssen, dass diese Typen anstelle der Standardeinstellungen für jedes Datumsfeld in Ihren POJOs verwendet werden sollen (und auch die reine JPA-Kompatibilität beeinträchtigen), es sei denn, jemand kennt eine allgemeinere Überschreibungsmethode.

UPDATE: Hibernate 3.6 hat die Typ-API geändert. In 3.6 habe ich eine Klasse UtcTimestampTypeDescriptor geschrieben, um dies zu implementieren.

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

Wenn Sie beim Starten der App TimestampTypeDescriptor.INSTANCE auf eine Instanz von UtcTimestampTypeDescriptor setzen, werden alle Zeitstempel gespeichert und als in UTC behandelt, ohne dass die Anmerkungen zu POJOs geändert werden müssen. [Ich habe das noch nicht getestet]

Divestoclimb
quelle
3
Wie weisen Sie Hibernate an, Ihre benutzerdefinierten Benutzer zu verwenden UtcTimestampType?
Derek Mahar
divestoclimb, mit welcher Version von Hibernate ist UtcTimestampTypekompatibel?
Derek Mahar
2
"ResultSet.getTimestamp und PreparedStatement.setTimestamp geben in ihren Dokumenten an, dass sie beim Lesen und Schreiben von / in die Datenbank standardmäßig Datumsangaben in / aus der aktuellen JVM-Zeitzone transformieren." Haben Sie eine Referenz? Ich sehe keine Erwähnung in den Java 6 Javadocs für diese Methoden. Laut stackoverflow.com/questions/4123534/… hängt es davon ab , wie diese Methoden die Zeitzone auf einen bestimmten Dateoder einen TimestampJDBC-Treiber anwenden .
Derek Mahar
1
Ich hätte schwören können, dass ich das über die Verwendung der JVM-Zeitzone im letzten Jahr gelesen habe, aber jetzt kann ich es nicht finden. Ich habe es möglicherweise in den Dokumenten für einen bestimmten JDBC-Treiber gefunden und verallgemeinert.
Divestoclimb
2
Damit die 3.6-Version Ihres Beispiels funktioniert, musste ich einen neuen Typ erstellen, der im Grunde ein Wrapper um den TimeStampType war, und diesen Typ dann auf dem Feld festlegen.
Shaun Stone
17

Verwenden Sie bei Spring Boot JPA den folgenden Code in Ihrer Datei application.properties, und natürlich können Sie die Zeitzone nach Ihren Wünschen ändern

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

Dann in Ihrer Entity-Klassendatei

@Column
private LocalDateTime created;
JavaGeek
quelle
11

Hinzufügen einer Antwort, die vollständig auf einem Hinweis von Shaun Stone basiert und dem Divestoclimb verpflichtet ist. Ich wollte es nur im Detail formulieren, da es ein häufiges Problem ist und die Lösung etwas verwirrend ist.

Dies verwendet Hibernate 4.1.4.Final, obwohl ich vermute, dass alles nach 3.6 funktionieren wird.

Erstellen Sie zunächst den UtcTimestampTypeDescriptor von 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 );
            }
        };
    }
}

Erstellen Sie dann UtcTimestampType, das UtcTimestampTypeDescriptor anstelle von TimestampTypeDescriptor als SqlTypeDescriptor im Super-Konstruktor-Aufruf verwendet, ansonsten aber alles an TimestampType delegiert:

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

Wenn Sie Ihre Hibernate-Konfiguration initialisieren, registrieren Sie UtcTimestampType als Typüberschreibung:

configuration.registerTypeOverride(new UtcTimestampType());

Jetzt sollten sich Zeitstempel nicht mehr mit der Zeitzone der JVM auf dem Weg zur und von der Datenbank befassen. HTH.

Shane
quelle
6
Es wäre toll, die Lösung für JPA und Spring Configuration zu sehen.
Aubergine
2
Ein Hinweis zur Verwendung dieses Ansatzes bei nativen Abfragen in Hibernate. Damit diese überschriebenen Typen verwendet werden können, müssen Sie den Wert mit query.setParameter (int pos, Objektwert) und nicht mit query.setParameter (int pos, Datumswert, TemporalType temporalType) festlegen. Wenn Sie Letzteres verwenden, verwendet Hibernate die ursprünglichen Typimplementierungen, da diese fest codiert sind.
Nigel
Wo soll ich die Anweisung configuration.registerTypeOverride (new UtcTimestampType ()) aufrufen? ?
Stony
@Stony Wo immer Sie Ihre Hibernate-Konfiguration initialisieren. Wenn Sie ein HibernateUtil haben (die meisten tun es), wird es dort drin sein.
Shane
1
Es funktioniert, aber nachdem ich es überprüft habe, habe ich festgestellt, dass es nicht für Postgres-Server erforderlich ist, die in timezone = "UTC" und mit allen Standardtypen von Zeitstempeln als "Zeitstempel mit Zeitzone" arbeiten (dies erfolgt dann automatisch). Aber hier ist eine feste Version für Hibernate 4.3.5 GA als komplette Einzelklasse und überschreibende Spring Factory Bean Pastebin.com/tT4ACXn6
Lukasz Frankowski
10

Sie würden denken, dass dieses häufige Problem von Hibernate behoben wird. Aber es ist nicht! Es gibt ein paar "Hacks", um es richtig zu machen.

Ich verwende das Datum als Long in der Datenbank. Ich arbeite also immer mit Millisekunden nach dem 1.1.70. Ich habe dann Getter und Setter in meiner Klasse, die nur Daten zurückgeben / akzeptieren. Die API bleibt also gleich. Der Nachteil ist, dass ich Longs in der Datenbank habe. Also mit SQL kann ich so ziemlich nur <,>, = Vergleiche machen - keine ausgefallenen Datumsoperatoren.

Ein anderer Ansatz besteht darin, einen benutzerdefinierten Zuordnungstyp wie hier beschrieben zu verwenden: http://www.hibernate.org/100.html

Ich denke, der richtige Weg, um damit umzugehen, ist die Verwendung eines Kalenders anstelle eines Datums. Mit dem Kalender können Sie die Zeitzone festlegen, bevor Sie fortfahren.

HINWEIS: Der alberne Stackoverflow lässt mich keinen Kommentar abgeben. Hier ist eine Antwort auf David A.

Wenn Sie dieses Objekt in Chicago erstellen:

new Date(0);

Der Ruhezustand bleibt als "31.12.1969 18:00:00" erhalten. Die Daten sollten keine Zeitzone haben, daher bin ich mir nicht sicher, warum die Anpassung vorgenommen werden soll.

Codefinger
quelle
1
Schande über mich! Sie hatten Recht und der Link aus Ihrem Beitrag erklärt es gut. Jetzt denke ich, dass meine Antwort einen negativen Ruf verdient :)
David a.
1
Überhaupt nicht. Sie haben mich ermutigt, ein sehr explizites Beispiel dafür zu veröffentlichen, warum dies ein Problem ist.
Codefinger
4
Ich konnte die Zeiten mit dem Kalenderobjekt korrekt beibehalten, sodass sie wie von Ihnen vorgeschlagen als UTC in der Datenbank gespeichert werden. Beim Zurücklesen persistenter Entitäten aus der Datenbank geht Hibernate jedoch davon aus, dass sie sich in der lokalen Zeitzone befinden und das Kalenderobjekt falsch ist!
John K
1
John K, um dieses CalendarLeseproblem zu lösen, sollte Hibernate oder JPA eine Möglichkeit bieten, für jede Zuordnung die Zeitzone anzugeben, in die Hibernate das Datum übersetzen soll, an dem es liest und in eine TIMESTAMPSpalte schreibt .
Derek Mahar
joekutner, nachdem ich stackoverflow.com/questions/4123534/… gelesen habe , teile ich Ihre Meinung, dass wir Millisekunden seit der Epoche in der Datenbank speichern sollten, anstatt a, Timestampda wir dem JDBC-Treiber nicht unbedingt vertrauen können, dass Daten als gespeichert werden Wir würden erwarten.
Derek Mahar
8

Hier sind mehrere Zeitzonen in Betrieb:

  1. Javas Date-Klassen (util und sql), die implizite Zeitzonen von UTC haben
  2. Die Zeitzone, in der Ihre JVM ausgeführt wird, und
  3. Die Standardzeitzone Ihres Datenbankservers.

All dies kann unterschiedlich sein. Hibernate / JPA weist einen schwerwiegenden Designmangel auf, da ein Benutzer nicht einfach sicherstellen kann, dass Zeitzoneninformationen auf dem Datenbankserver gespeichert werden (was die Rekonstruktion korrekter Zeiten und Daten in der JVM ermöglicht).

Ohne die Möglichkeit, die Zeitzone mithilfe von JPA / Hibernate (einfach) zu speichern, gehen Informationen verloren, und sobald Informationen verloren gehen, wird die Erstellung teuer (wenn überhaupt möglich).

Ich würde argumentieren, dass es besser ist, immer Zeitzoneninformationen zu speichern (sollte die Standardeinstellung sein) und Benutzer dann die optionale Möglichkeit haben sollten, die Zeitzone zu optimieren (obwohl dies nur die Anzeige wirklich beeinflusst, gibt es in jedem Datum immer noch eine implizite Zeitzone).

Entschuldigung, dieser Beitrag bietet keine Problemumgehung (die an anderer Stelle beantwortet wurde), aber es ist eine Rationalisierung, warum es wichtig ist, immer Zeitzoneninformationen zu speichern. Leider scheinen viele Informatiker und Programmierer gegen die Notwendigkeit von Zeitzonen zu argumentieren, nur weil sie die Perspektive des "Informationsverlusts" nicht schätzen und wissen, dass dies Dinge wie die Internationalisierung sehr schwierig macht - was heutzutage bei Websites, auf die zugegriffen werden kann, sehr wichtig ist Kunden und Personen in Ihrer Organisation, die sich auf der ganzen Welt bewegen.

Moa
quelle
1
"Hibernate / JPA weist einen schwerwiegenden Designmangel auf" Ich würde sagen, es handelt sich um einen SQL-Mangel, der traditionell dazu geführt hat, dass die Zeitzone implizit ist und daher möglicherweise alles. Dummes SQL.
Raedwald
3
Anstatt immer die Zeitzone zu speichern, können Sie auch eine Zeitzone (normalerweise UTC) standardisieren und alles in diese Zeitzone konvertieren, wenn Sie fortbestehen (und zurück, wenn Sie lesen). Das machen wir normalerweise. JDBC unterstützt dies jedoch auch nicht direkt: - /.
Sleske
3

Bitte werfen Sie einen Blick auf mein Projekt auf Sourceforge, das Benutzertypen für Standard-SQL-Datums- und Uhrzeittypen sowie JSR 310 und Joda Time enthält. Alle Typen versuchen, das Ausgleichsproblem zu lösen. Siehe http://sourceforge.net/projects/usertype/

EDIT: Als Antwort auf die Frage von Derek Mahar, die diesem Kommentar beigefügt ist:

"Chris, arbeiten Ihre Benutzertypen mit Hibernate 3 oder höher? - Derek Mahar 7. November 10 um 12:30 Uhr."

Ja, diese Typen unterstützen Hibernate 3.x-Versionen, einschließlich Hibernate 3.6.

Chris Pheby
quelle
2

Das Datum liegt nicht in einer Zeitzone (es ist ein Millisekunden-Büro ab einem definierten Zeitpunkt, der für alle gleich ist), aber zugrunde liegende (R) DBs speichern Zeitstempel im Allgemeinen in politischem Format (Jahr, Monat, Tag, Stunde, Minute, Sekunde ,. ..) das ist zeitzonensensitiv.

Um es ernst zu nehmen, MUSS Hibernate innerhalb einer Form der Zuordnung mitgeteilt werden, dass sich das DB-Datum in der einen oder anderen Zeitzone befindet, damit es beim Laden oder Speichern keine eigene ...


quelle
1

Ich hatte genau das gleiche Problem, als ich die Daten in der Datenbank als UTC speichern und die Verwendung varcharund explizite String <-> java.util.DateKonvertierung vermeiden oder meine gesamte Java-App in der UTC-Zeitzone einstellen wollte (da dies zu weiteren unerwarteten Problemen führen könnte, wenn die JVM vorhanden ist in vielen Anwendungen geteilt).

Es gibt also ein Open Source-Projekt DbAssist, mit dem Sie das Lesen / Schreiben als UTC-Datum aus der Datenbank einfach festlegen können. Da Sie JPA-Anmerkungen verwenden, um die Felder in der Entität zuzuordnen, müssen Sie lediglich die folgende Abhängigkeit in Ihre Maven- pomDatei aufnehmen:

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

Anschließend wenden Sie das Update an (Beispiel für den Ruhezustand + Spring Boot), indem Sie @EnableAutoConfigurationvor der Spring-Anwendungsklasse eine Anmerkung hinzufügen . Weitere Installationsanweisungen für Setups und weitere Anwendungsbeispiele finden Sie im Github des Projekts .

Das Gute ist, dass Sie die Entitäten überhaupt nicht ändern müssen. Sie können ihre java.util.DateFelder so lassen, wie sie sind.

5.2.2muss der von Ihnen verwendeten Hibernate-Version entsprechen. Ich bin nicht sicher, welche Version Sie in Ihrem Projekt verwenden, aber die vollständige Liste der bereitgestellten Korrekturen finden Sie auf der Wiki-Seite des Githubs des Projekts . Der Grund, warum das Update für verschiedene Hibernate-Versionen unterschiedlich ist, liegt darin, dass Hibernate-Ersteller die API zwischen den Releases einige Male geändert haben.

Intern verwendet das Update Hinweise von Divestoclimb, Shane und einigen anderen Quellen, um eine benutzerdefinierte Version zu erstellen UtcDateType. Anschließend wird der Standard java.util.Datedem Benutzer zugeordnet, UtcDateTypeder alle erforderlichen Zeitzonen behandelt. Die Zuordnung der Typen erfolgt mithilfe von @TypedefAnmerkungen in der bereitgestellten package-info.javaDatei.

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

Hier finden Sie einen Artikel , in dem erklärt wird, warum eine solche Zeitverschiebung überhaupt auftritt und wie diese gelöst werden kann.

orim
quelle
1

Im Ruhezustand können keine Zeitzonen durch Anmerkungen oder auf andere Weise angegeben werden. Wenn Sie Kalender anstelle von Datum verwenden, können Sie eine Problemumgehung mithilfe der HIbernate-Eigenschaft AccessType implementieren und die Zuordnung selbst implementieren. Die fortgeschrittenere Lösung besteht darin, einen benutzerdefinierten Benutzertyp zu implementieren, um Ihr Datum oder Ihren Kalender zuzuordnen. Beide Lösungen werden in meinem Blogbeitrag hier erklärt: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

Joobik
quelle