Come memorizzare data / ora e timestamp nel fuso orario UTC con JPA e Hibernate


106

Come posso configurare JPA / Hibernate per memorizzare una data / ora nel database come fuso orario UTC (GMT)? Considera questa entità JPA annotata:

public class Event {
    @Id
    public int id;

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

Se la data è 2008-Feb-03 9:30 am Pacific Standard Time (PST), allora voglio che l'ora UTC 2008-Feb-03 17:30 pm sia memorizzata nel database. Allo stesso modo, quando la data viene recuperata dal database, voglio che venga interpretata come UTC. Quindi in questo caso le 530pm sono le 530pm UTC. Quando viene visualizzato, verrà formattato come 9:30 PST.


1
La risposta di Vlad Mihalcea fornisce una risposta aggiornata (per Hibernate 5.2+)
Dinei

Risposte:


73

Con Hibernate 5.2, ora puoi forzare il fuso orario UTC utilizzando la seguente proprietà di configurazione:

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

Per maggiori dettagli, consulta questo articolo .


19
Ho scritto anche quello: D Ora, indovina chi ha aggiunto il supporto per questa funzione in Hibernate?
Vlad Mihalcea

Oh, solo ora ho capito che il nome e l'immagine sono gli stessi nel tuo profilo e in quegli articoli ... Ottimo lavoro Vlad :)
Dinei

@VladMihalcea se è per Mysql, è necessario dire a MySql di utilizzare il fuso orario utilizzando useTimezone=truenella stringa di connessione. Solo allora Proprietà Ambito hibernate.jdbc.time_zonefunzionerà
TheCoder

In realtà, devi impostare useLegacyDatetimeCodesu false
Vlad Mihalcea

2
hibernate.jdbc.time_zone sembra essere ignorato o non ha alcun effetto se usato con PostgreSQL
Alex R

48

Per quanto ne so, devi mettere l'intera app Java nel fuso orario UTC (in modo che Hibernate memorizzi le date in UTC) e dovrai convertirlo in qualsiasi fuso orario desiderato quando visualizzi le cose (almeno lo facciamo per di qua).

All'avvio, facciamo:

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

E imposta il fuso orario desiderato su DateFormat:

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

5
mitchnull, la tua soluzione non funzionerà in tutti i casi perché Hibernate delega l'impostazione delle date al driver JDBC e ogni driver JDBC gestisce date e fusi orari in modo diverso. vedi stackoverflow.com/questions/4123534/… .
Derek Mahar

2
Ma se avvio la mia app informando alla proprietà JVM "-Duser.timezone = + 00: 00", non è lo stesso comportarsi?
rafa.ferreira

8
Per quanto ne so, funzionerà in tutti i casi tranne quando la JVM e il server database si trovano in fusi orari diversi.
Shane

stevekuo e @mitchnull vedono la soluzione divestoclimb di seguito che è molto più migliore ea prova di effetti collaterali stackoverflow.com/a/3430957/233906
Cerber

HibernateJPA supporta le annotazioni "@Factory" e "@Externalizer", è il modo in cui gestisco utc datetime nella libreria OpenJPA. stackoverflow.com/questions/10819862/...
whome

44

Hibernate ignora le cose sul fuso orario in Date (perché non ce n'è), ma in realtà è il livello JDBC che sta causando problemi. ResultSet.getTimestamped PreparedStatement.setTimestampentrambi dicono nei loro documenti che trasformano le date nel / dal fuso orario JVM corrente per impostazione predefinita durante la lettura e la scrittura dal / al database.

Ho trovato una soluzione a questo in Hibernate 3.5 creando sottoclassi org.hibernate.type.TimestampTypeche costringono questi metodi JDBC a utilizzare UTC invece del fuso orario locale:

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

La stessa cosa dovrebbe essere fatta per correggere TimeType e DateType se usi questi tipi. Lo svantaggio è che dovrai specificare manualmente che questi tipi devono essere utilizzati al posto dei valori predefiniti su ogni campo Data nei tuoi POJO (e interrompe anche la pura compatibilità JPA), a meno che qualcuno non conosca un metodo di override più generale.

AGGIORNAMENTO: Hibernate 3.6 ha cambiato i tipi di API. In 3.6, ho scritto una classe UtcTimestampTypeDescriptor per implementarlo.

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

Ora, quando l'app si avvia, se imposti TimestampTypeDescriptor.INSTANCE su un'istanza di UtcTimestampTypeDescriptor, tutti i timestamp verranno archiviati e trattati come in UTC senza dover modificare le annotazioni sui POJO. [Non l'ho ancora testato]


3
Come dici a Hibernate di usare la tua custom UtcTimestampType?
Derek Mahar

divestoclimb, con quale versione di Hibernate è UtcTimestampTypecompatibile?
Derek Mahar

2
"ResultSet.getTimestamp e PreparedStatement.setTimestamp dicono entrambi nei loro documenti che trasformano le date nel / dal fuso orario JVM corrente per impostazione predefinita durante la lettura e la scrittura dal / al database." Hai una referenza? Non vedo alcuna menzione di questo nei Javadoc Java 6 per questi metodi. Secondo stackoverflow.com/questions/4123534/… , il modo in cui questi metodi applicano il fuso orario a un dato Dateo Timestampdipende dal driver JDBC.
Derek Mahar

1
Avrei giurato di averlo letto sull'uso del fuso orario JVM l'anno scorso, ma ora non riesco a trovarlo. Potrei averlo trovato sui documenti per un driver JDBC specifico e generalizzato.
divestoclimb

2
Per far funzionare la versione 3.6 del tuo esempio, ho dovuto creare un nuovo tipo che era fondamentalmente un wrapper attorno al TimeStampType, quindi impostare quel tipo sul campo.
Shaun Stone

17

Con Spring Boot JPA, usa il codice seguente nel tuo file application.properties e ovviamente puoi modificare il fuso orario a tua scelta

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

Quindi nel tuo file di classe Entity,

@Column
private LocalDateTime created;

11

Aggiungendo una risposta che è completamente basata e in debito con il divestoclimb con un suggerimento di Shaun Stone. Volevo solo spiegarlo in dettaglio poiché è un problema comune e la soluzione è un po 'confusa.

Questo sta usando Hibernate 4.1.4.Final, anche se sospetto che qualsiasi cosa dopo la 3.6 funzionerà.

Innanzitutto, crea UtcTimestampTypeDescriptor di 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 );
            }
        };
    }
}

Quindi creare UtcTimestampType, che utilizza UtcTimestampTypeDescriptor invece di TimestampTypeDescriptor come SqlTypeDescriptor nella chiamata del super costruttore ma altrimenti delega tutto a 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);
    }
}

Infine, quando si inizializza la configurazione di Hibernate, registrare UtcTimestampType come override del tipo:

configuration.registerTypeOverride(new UtcTimestampType());

Ora i timestamp non dovrebbero essere interessati al fuso orario della JVM nel loro percorso da e verso il database. HTH.


6
Sarebbe bello vedere la soluzione per JPA e Spring Configuration.
Melanzane

2
Una nota sull'utilizzo di questo approccio con le query native in Hibernate. Per utilizzare questi tipi sostituiti, è necessario impostare il valore con query.setParameter (int pos, Object value), non query.setParameter (int pos, Date value, TemporalType temporalType). Se usi quest'ultimo, Hibernate userà le sue implementazioni di tipo originale, poiché sono hardcoded.
Nigel

Dove dovrei chiamare l'istruzione configuration.registerTypeOverride (new UtcTimestampType ()); ?
Stony

@Stony Ovunque inizializzi la configurazione di Hibernate. Se hai un HibernateUtil (la maggior parte lo fa), sarà lì.
Shane

1
Funziona, ma dopo averlo controllato mi sono reso conto che non è richiesto per il server postgres che funziona in timezone = "UTC" e con tutti i tipi di timestamp predefiniti come "timestamp with time zone" (viene eseguito automaticamente quindi). Ma ecco la versione fissa per Hibernate 4.3.5 GA come classe singola completa e overridding spring factory bean pastebin.com/tT4ACXn6
Lukasz Frankowski

10

Penseresti che questo problema comune sarebbe stato risolto da Hibernate. Ma non lo è! Ci sono alcuni "trucchi" per farlo bene.

Quello che uso è quello di memorizzare la data come un lungo nel database. Quindi lavoro sempre con millisecondi dopo 1/1/70. Quindi ho getter e setter sulla mia classe che restituiscono / accettano solo date. Quindi l'API rimane la stessa. Il lato negativo è che ho dei long nel database. Quindi con SQL posso fare praticamente solo <,>, = confronti - non operatori di data di fantasia.

Un altro approccio consiste nell'utilizzare un tipo di mappatura personalizzato come descritto qui: http://www.hibernate.org/100.html

Penso che il modo corretto per affrontare questo problema sia usare un calendario invece di una data. Con il calendario è possibile impostare il fuso orario prima di persistere.

NOTA: lo sciocco stackoverflow non mi consente di commentare, quindi ecco una risposta a david a.

Se crei questo oggetto a Chicago:

new Date(0);

Hibernate persiste come "12/31/1969 18:00:00". Le date dovrebbero essere prive di fuso orario, quindi non sono sicuro del motivo per cui dovrebbe essere effettuato l'adeguamento.


1
Mi vergogno! Avevi ragione e il link dal tuo post lo spiega bene. Ora immagino che la mia risposta meriti una reputazione negativa :)
david a.

1
Affatto. mi hai incoraggiato a postare un esempio molto esplicito del perché questo è un problema.
codefinger

4
Sono stato in grado di mantenere i tempi correttamente utilizzando l'oggetto Calendar in modo che siano memorizzati nel DB come UTC come suggerito. Tuttavia, durante la lettura di entità persistenti dal database, Hibernate presume che si trovino nel fuso orario locale e che l'oggetto Calendar non sia corretto!
John K

1
John K, per risolvere questo Calendarproblema di lettura, penso che Hibernate o JPA dovrebbero fornire un modo per specificare, per ogni mappatura, il fuso orario in cui Hibernate dovrebbe tradurre la data in cui legge e scrive in una TIMESTAMPcolonna.
Derek Mahar

joekutner, dopo aver letto stackoverflow.com/questions/4123534/... , sono venuto a condividere la tua opinione che dovremmo conservare millisecondi fin dall'epoca nel database, piuttosto che un Timestampdato che non può necessariamente fidarsi del driver JDBC alle date dei negozi come ci aspetteremmo.
Derek Mahar

8

Ci sono diversi fusi orari in funzione qui:

  1. Le classi Date di Java (util e sql), che hanno fusi orari impliciti di UTC
  2. Il fuso orario in cui è in esecuzione la JVM e
  3. il fuso orario predefinito del server del database.

Tutti questi possono essere diversi. Hibernate / JPA presenta una grave carenza di progettazione in quanto un utente non può garantire facilmente che le informazioni sul fuso orario siano conservate nel server del database (che consente la ricostruzione di orari e date corretti nella JVM).

Senza la capacità di memorizzare (facilmente) il fuso orario utilizzando JPA / Hibernate, le informazioni vengono perse e una volta perse le informazioni diventa costoso costruirle (se possibile).

Direi che è meglio memorizzare sempre le informazioni sul fuso orario (dovrebbe essere l'impostazione predefinita) e gli utenti dovrebbero quindi avere la possibilità opzionale di ottimizzare il fuso orario (sebbene influisca solo sulla visualizzazione, c'è ancora un fuso orario implicito in qualsiasi data).

Spiacenti, questo post non fornisce una soluzione (a cui è stata data risposta altrove) ma è una razionalizzazione del motivo per cui è importante memorizzare sempre le informazioni sul fuso orario. Sfortunatamente sembra che molti informatici e professionisti della programmazione si oppongano alla necessità di fusi orari semplicemente perché non apprezzano la prospettiva della "perdita di informazioni" e come ciò renda molto difficili cose come l'internazionalizzazione, cosa molto importante oggigiorno con i siti web accessibili da clienti e persone della tua organizzazione mentre si spostano nel mondo.


1
"Hibernate / JPA ha una grave carenza di progettazione" Direi che è una carenza di SQL, che tradizionalmente ha consentito che il fuso orario fosse implicito, e quindi potenzialmente qualsiasi cosa. SQL stupido.
Raedwald

3
In realtà, invece di memorizzare sempre il fuso orario, puoi anche standardizzare un fuso orario (di solito UTC) e convertire tutto in questo fuso orario quando persiste (e viceversa durante la lettura). Questo è quello che facciamo di solito. Tuttavia, JDBC non supporta direttamente neanche quello: - /.
sleske

3

Dai un'occhiata al mio progetto su Sourceforge che ha tipi di utente per i tipi di data e ora SQL standard, oltre a JSR 310 e Joda Time. Tutti i tipi cercano di risolvere il problema della compensazione. Vedi http://sourceforge.net/projects/usertype/

EDIT: In risposta alla domanda di Derek Mahar allegata a questo commento:

"Chris, i tuoi tipi di utente funzionano con Hibernate 3 o versioni successive? - Derek Mahar 7 novembre 10 alle 12:30"

Sì, questi tipi supportano le versioni di Hibernate 3.x incluso Hibernate 3.6.


2

La data non è in alcun fuso orario (è un millisecondo da un momento definito nello stesso tempo per tutti), ma i DB (R) sottostanti generalmente memorizzano i timestamp in formato politico (anno, mese, giorno, ora, minuto, secondo,. ..) che è sensibile al fuso orario.

Per essere seri, Hibernate DEVE essere autorizzato a dire all'interno di una qualche forma di mappatura che la data del DB è in questo e tale fuso orario in modo che quando la carica o la memorizza non assume il proprio ...


1

Ho riscontrato lo stesso problema quando volevo memorizzare le date nel DB come UTC ed evitare di utilizzare conversioni varcharesplicite String <-> java.util.Dateo impostare tutta la mia app Java nel fuso orario UTC (perché questo potrebbe portare a altri problemi imprevisti, se la JVM è condiviso tra molte applicazioni).

Quindi, esiste un progetto open source DbAssist, che ti consente di correggere facilmente la lettura / scrittura come data UTC dal database. Poiché stai utilizzando le annotazioni JPA per mappare i campi nell'entità, tutto ciò che devi fare è includere la seguente dipendenza al tuo pomfile Maven :

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

Quindi si applica la correzione (per l'esempio Hibernate + Spring Boot) aggiungendo @EnableAutoConfigurationun'annotazione prima della classe dell'applicazione Spring. Per le istruzioni di installazione di altre configurazioni e altri esempi di utilizzo, fare riferimento al github del progetto .

La cosa buona è che non devi modificare affatto le entità; puoi lasciare i loro java.util.Datecampi così come sono.

5.2.2deve corrispondere alla versione di Hibernate che stai utilizzando. Non sono sicuro di quale versione stai utilizzando nel tuo progetto, ma l'elenco completo delle correzioni fornite è disponibile sulla pagina wiki del github del progetto . Il motivo per cui la correzione è diversa per le varie versioni di Hibernate è perché i creatori di Hibernate hanno cambiato l'API un paio di volte tra le versioni.

Internamente, la correzione utilizza suggerimenti da divestoclimb, Shane e poche altre fonti per creare un custom UtcDateType. Quindi mappa lo standard java.util.Datecon l'usanza UtcDateTypeche gestisce tutta la gestione del fuso orario necessaria. La mappatura dei tipi si ottiene utilizzando l' @Typedefannotazione nel package-info.javafile fornito .

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

Puoi trovare un articolo qui che spiega perché si verifica un tale spostamento temporale e quali sono gli approcci per risolverlo.


1

Hibernate non consente di specificare i fusi orari tramite annotazioni o altri mezzi. Se utilizzi Calendar invece della data, puoi implementare una soluzione alternativa utilizzando la proprietà HIbernate AccessType e implementando tu stesso il mapping. La soluzione più avanzata consiste nell'implementare un UserType personalizzato per mappare la data o il calendario. Entrambe le soluzioni sono spiegate nel mio post sul blog qui: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.