Comment stocker la date / l'heure et les horodatages dans le fuseau horaire UTC avec JPA et Hibernate

106

Comment puis-je configurer JPA / Hibernate pour stocker une date / heure dans la base de données en tant que fuseau horaire UTC (GMT)? Considérez cette entité JPA annotée:

public class Event {
    @Id
    public int id;

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

Si la date est le 03 février 2008 à 9 h 30, heure normale du Pacifique (PST), je souhaite que l'heure UTC du 03 février 2008 à 17 h 30 soit stockée dans la base de données. De même, lorsque la date est extraite de la base de données, je veux qu'elle soit interprétée comme UTC. Donc, dans ce cas, 17h30 est 17h30 UTC. Lorsqu'il est affiché, il sera formaté à 9h30 PST.

Steve Kuo
la source
1
La réponse de Vlad Mihalcea fournit une réponse mise à jour (pour Hibernate 5.2+)
Dinei

Réponses:

73

Avec Hibernate 5.2, vous pouvez désormais forcer le fuseau horaire UTC à l'aide de la propriété de configuration suivante:

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

Pour plus de détails, consultez cet article .

Vlad Mihalcea
la source
19
J'ai aussi écrit celui-là: D Maintenant, devinez qui a ajouté la prise en charge de cette fonctionnalité dans Hibernate?
Vlad Mihalcea
Oh, je viens de réaliser que le nom et la photo sont les mêmes dans votre profil et ces articles ... Bon travail Vlad :)
Dinei
@VladMihalcea si c'est pour Mysql, il faut dire à MySql d'utiliser le fuseau horaire en utilisant useTimezone=truedans la chaîne de connexion. Ensuite, seule la définition de la propriété hibernate.jdbc.time_zonefonctionnera
TheCoder
En fait, vous devez définir useLegacyDatetimeCodesur false
Vlad Mihalcea
2
hibernate.jdbc.time_zone semble être ignoré ou n'a aucun effet lorsqu'il est utilisé avec PostgreSQL
Alex R
48

À ma connaissance, vous devez mettre toute votre application Java dans le fuseau horaire UTC (afin qu'Hibernate stocke les dates en UTC), et vous devrez convertir le fuseau horaire souhaité lorsque vous affichez des éléments (au moins nous le faisons par ici).

Au démarrage, nous faisons:

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

Et définissez le fuseau horaire souhaité sur DateFormat:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
mitchnull
la source
5
mitchnull, votre solution ne fonctionnera pas dans tous les cas car Hibernate délègue la définition des dates au pilote JDBC et chaque pilote JDBC gère les dates et les fuseaux horaires différemment. voir stackoverflow.com/questions/4123534/… .
Derek Mahar
2
Mais si je lance mon application en informant la propriété JVM "-Duser.timezone = + 00: 00", le comportement n'est-il pas le même?
rafa.ferreira
8
Pour autant que je sache, cela fonctionnera dans tous les cas, sauf lorsque la JVM et le serveur de base de données sont dans des fuseaux horaires différents.
Shane
stevekuo et @mitchnull voient la solution de désinvestissement ci-dessous, qui est bien meilleure et à l' épreuve des effets secondaires stackoverflow.com/a/3430957/233906
Cerber
Est-ce que HibernateJPA prend en charge les annotations «@Factory» et «@Externalizer», c'est ainsi que je gère la gestion de datetime utc dans la bibliothèque OpenJPA. stackoverflow.com/questions/10819862/…
Whome
44

Hibernate ignore les éléments de fuseau horaire dans Dates (car il n'y en a pas), mais c'est en fait la couche JDBC qui pose des problèmes. ResultSet.getTimestampet les PreparedStatement.setTimestampdeux disent dans leur documentation qu'ils transforment les dates vers / depuis le fuseau horaire JVM actuel par défaut lors de la lecture et de l'écriture depuis / vers la base de données.

J'ai trouvé une solution à cela dans Hibernate 3.5 en sous org.hibernate.type.TimestampType- classant qui oblige ces méthodes JDBC à utiliser UTC au lieu du fuseau horaire local:

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 même chose doit être faite pour corriger TimeType et DateType si vous utilisez ces types. L'inconvénient est que vous devrez spécifier manuellement que ces types doivent être utilisés à la place des valeurs par défaut sur chaque champ Date de vos POJO (et interrompent également la compatibilité JPA pure), à ​​moins que quelqu'un ne connaisse une méthode de remplacement plus générale.

MISE À JOUR: Hibernate 3.6 a changé l'API des types. En 3.6, j'ai écrit une classe UtcTimestampTypeDescriptor pour l'implémenter.

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

Désormais, lorsque l'application démarre, si vous définissez TimestampTypeDescriptor.INSTANCE sur une instance de UtcTimestampTypeDescriptor, tous les horodatages seront stockés et traités comme étant en UTC sans avoir à modifier les annotations sur les POJO. [Je n'ai pas encore testé cela]

se désengager
la source
3
Comment dites-vous à Hibernate d'utiliser votre coutume UtcTimestampType?
Derek Mahar
divestoclimb, avec quelle version d'Hibernate est UtcTimestampTypecompatible?
Derek Mahar
2
"ResultSet.getTimestamp et PreparedStatement.setTimestamp indiquent tous deux dans leurs documents qu'ils transforment les dates vers / depuis le fuseau horaire JVM actuel par défaut lors de la lecture et de l'écriture depuis / vers la base de données." Avez-vous une référence? Je ne vois aucune mention de cela dans les Javadocs Java 6 pour ces méthodes. Selon stackoverflow.com/questions/4123534/… , comment ces méthodes appliquent le fuseau horaire à un pilote donné Dateou Timestampdépendent du pilote JDBC.
Derek Mahar
1
J'aurais pu jurer avoir lu cela sur l'utilisation du fuseau horaire JVM l'année dernière, mais maintenant je ne le trouve pas. J'aurais pu le trouver sur la documentation pour un pilote JDBC spécifique et généralisé.
désinvestir le
2
Pour que la version 3.6 de votre exemple fonctionne, j'ai dû créer un nouveau type qui était essentiellement un wrapper autour du TimeStampType, puis définir ce type sur le champ.
Shaun Stone
17

Avec Spring Boot JPA, utilisez le code ci-dessous dans votre fichier application.properties et vous pouvez évidemment modifier le fuseau horaire à votre choix

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

Puis dans votre fichier de classe Entity,

@Column
private LocalDateTime created;
JavaGeek
la source
11

Ajout d'une réponse qui est entièrement basée sur et doit se désengager avec un indice de Shaun Stone. Je voulais juste l'expliquer en détail car c'est un problème courant et la solution est un peu déroutante.

Ceci utilise Hibernate 4.1.4.Final, bien que je soupçonne que tout ce qui sera après 3.6 fonctionnera.

Commencez par créer UtcTimestampTypeDescriptor de 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 );
            }
        };
    }
}

Ensuite, créez UtcTimestampType, qui utilise UtcTimestampTypeDescriptor au lieu de TimestampTypeDescriptor comme SqlTypeDescriptor dans l'appel du super constructeur mais délègue sinon tout à 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);
    }
}

Enfin, lorsque vous initialisez votre configuration Hibernate, enregistrez UtcTimestampType comme remplacement de type:

configuration.registerTypeOverride(new UtcTimestampType());

Désormais, les horodatages ne devraient pas être concernés par le fuseau horaire de la JVM sur le chemin de et vers la base de données. HTH.

Shane
la source
6
Ce serait formidable de voir la solution pour JPA et Spring Configuration.
Aubergine
2
Une note concernant l'utilisation de cette approche avec des requêtes natives dans Hibernate. Pour utiliser ces types remplacés, vous devez définir la valeur avec query.setParameter (int pos, Object value), et non query.setParameter (int pos, Date value, TemporalType temporalType). Si vous utilisez ce dernier, Hibernate utilisera ses implémentations de type d'origine, car elles sont codées en dur.
Nigel
Où dois-je appeler l'instruction configuration.registerTypeOverride (new UtcTimestampType ()); ?
Stony
@Stony Partout où vous initialisez votre configuration Hibernate. Si vous avez un HibernateUtil (la plupart le font), il sera là-dedans.
Shane
1
Cela fonctionne, mais après l'avoir vérifié, j'ai réalisé que ce n'est pas nécessaire pour le serveur postgres fonctionnant dans timezone = "UTC" et avec tous les types d'horodatage par défaut comme "horodatage avec fuseau horaire" (c'est alors fait automatiquement). Mais, voici la version corrigée pour Hibernate 4.3.5 GA en tant que bean de classe unique et remplaçant le bean factory de printemps pastebin.com/tT4ACXn6
Lukasz Frankowski
10

On pourrait penser que ce problème commun serait pris en charge par Hibernate. Mais ce n'est pas! Il y a quelques "hacks" pour bien faire les choses.

Celui que j'utilise est de stocker la date sous forme de long dans la base de données. Donc je travaille toujours avec des millisecondes après le 1/1/70. J'ai alors des getters et des setters sur ma classe qui retournent / n'acceptent que des dates. Ainsi, l'API reste la même. L'inconvénient est que j'ai longtemps dans la base de données. SO avec SQL, je ne peux pratiquement faire que des comparaisons <,>, = - pas des opérateurs de date sophistiqués.

Une autre approche consiste à utiliser un type de mappage personnalisé comme décrit ici: http://www.hibernate.org/100.html

Je pense que la bonne façon de gérer cela est d'utiliser un calendrier au lieu d'une date. Avec le calendrier, vous pouvez définir le fuseau horaire avant de persister.

REMARQUE: Stackoverflow stupide ne me laisse pas commenter, voici donc une réponse à david a.

Si vous créez cet objet à Chicago:

new Date(0);

Hibernate le persiste sous le nom "31/12/1969 18:00:00". Les dates doivent être dépourvues de fuseau horaire, je ne sais donc pas pourquoi l'ajustement serait effectué.

codefinger
la source
1
Honte sur moi! Vous aviez raison et le lien de votre message l'explique bien. Maintenant, je suppose que ma réponse mérite une réputation négative :)
david a.
1
Pas du tout. vous m'avez encouragé à publier un exemple très explicite des raisons pour lesquelles c'est un problème.
codefinger
4
J'ai pu conserver les temps correctement en utilisant l'objet Calendar afin qu'ils soient stockés dans la base de données au format UTC comme vous l'avez suggéré. Cependant, lors de la lecture des entités persistantes à partir de la base de données, Hibernate suppose qu'elles sont dans le fuseau horaire local et que l'objet Calendar est incorrect!
John K
1
John K, afin de résoudre ce Calendarproblème de lecture, je pense qu'Hibernate ou JPA devrait fournir un moyen de spécifier, pour chaque mappage, le fuseau horaire vers lequel Hibernate doit traduire la date à laquelle il lit et écrit dans une TIMESTAMPcolonne.
Derek Mahar
joekutner, après avoir lu stackoverflow.com/questions/4123534/... , je suis venu pour partager votre opinion que nous devrions stocker les millisecondes depuis l'Epoch dans la base de données plutôt que Timestampcar nous ne pouvons pas nécessairement faire confiance au pilote JDBC pour stocker les dates comme nous nous attendrions.
Derek Mahar
8

Il y a plusieurs fuseaux horaires en fonctionnement ici:

  1. Classes de date de Java (util et sql), qui ont des fuseaux horaires implicites de UTC
  2. Le fuseau horaire dans lequel votre JVM s'exécute, et
  3. le fuseau horaire par défaut de votre serveur de base de données.

Tout cela peut être différent. Hibernate / JPA présente un grave défaut de conception en ce qu'un utilisateur ne peut pas facilement garantir que les informations de fuseau horaire sont conservées dans le serveur de base de données (ce qui permet la reconstruction des heures et des dates correctes dans la JVM).

Sans la possibilité de stocker (facilement) le fuseau horaire à l'aide de JPA / Hibernate, les informations sont perdues et une fois les informations perdues, leur construction devient coûteuse (si possible).

Je dirais qu'il est préférable de toujours stocker les informations de fuseau horaire (devrait être la valeur par défaut) et que les utilisateurs devraient alors avoir la possibilité facultative d'optimiser le fuseau horaire (bien que cela n'affecte vraiment que l'affichage, il y a toujours un fuseau horaire implicite dans n'importe quelle date).

Désolé, cet article ne fournit pas de solution de contournement (cela a été répondu ailleurs), mais il explique pourquoi il est important de toujours stocker les informations de fuseau horaire. Malheureusement, il semble que de nombreux informaticiens et spécialistes de la programmation s'opposent au besoin de fuseaux horaires simplement parce qu'ils n'apprécient pas la perspective de la «perte d'informations» et comment cela rend des choses comme l'internationalisation très difficiles - ce qui est très important de nos jours avec des sites Web accessibles par les clients et les membres de votre organisation lorsqu'ils se déplacent à travers le monde.

Moa
la source
1
"Hibernate / JPA a un grave défaut de conception" Je dirais que c'est une carence en SQL, qui a traditionnellement permis au fuseau horaire d'être implicite, et donc potentiellement n'importe quoi. SQL idiot.
Raedwald
3
En fait, au lieu de toujours stocker le fuseau horaire, vous pouvez également normaliser sur un fuseau horaire (généralement UTC) et tout convertir dans ce fuseau horaire lors de la persistance (et inversement lors de la lecture). C'est ce que nous faisons habituellement. Cependant, JDBC ne prend pas en charge cela directement non plus: - /.
sleske
3

Veuillez jeter un œil à mon projet sur Sourceforge qui contient des types d'utilisateurs pour les types de date et d'heure SQL standard ainsi que JSR 310 et Joda Time. Tous les types tentent de résoudre le problème de la compensation. Voir http://sourceforge.net/projects/usertype/

EDIT: En réponse à la question de Derek Mahar jointe à ce commentaire:

"Chris, est-ce que vos types d'utilisateurs fonctionnent avec Hibernate 3 ou une version supérieure? - Derek Mahar 7 novembre 2010 à 12h30"

Oui, ces types prennent en charge les versions Hibernate 3.x, y compris Hibernate 3.6.

Chris Pheby
la source
2

La date ne se trouve dans aucun fuseau horaire (il s'agit d'un bureau de la milliseconde à partir d'un moment défini dans le temps, identique pour tout le monde), mais les bases de données (R) sous-jacentes stockent généralement les horodatages au format politique (année, mois, jour, heure, minute, seconde,. ..) qui est sensible au fuseau horaire.

Pour être sérieux, Hibernate DOIT être autorisé à se faire dire dans une forme de mappage que la date de la base de données est dans tel ou tel fuseau horaire de sorte que lorsqu'il la charge ou la stocke, elle n'assume pas la sienne ...


la source
1

J'ai rencontré le même problème lorsque je voulais stocker les dates dans la base de données au format UTC et éviter d'utiliser varchardes String <-> java.util.Dateconversions explicites , ou de définir toute mon application Java dans le fuseau horaire UTC (car cela pourrait entraîner d'autres problèmes inattendus, si la JVM est partagé entre de nombreuses applications).

Il existe donc un projet open source DbAssist, qui vous permet de corriger facilement la lecture / écriture en date UTC à partir de la base de données. Puisque vous utilisez des annotations JPA pour mapper les champs de l'entité, il vous suffit d'inclure la dépendance suivante dans votre pomfichier Maven :

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

Ensuite, vous appliquez le correctif (pour l'exemple Hibernate + Spring Boot) en ajoutant une @EnableAutoConfigurationannotation avant la classe d'application Spring. Pour obtenir d'autres instructions d'installation et d'autres exemples d'utilisation, reportez-vous simplement au github du projet .

La bonne chose est que vous n'avez pas du tout à modifier les entités; vous pouvez laisser leurs java.util.Datechamps tels quels.

5.2.2doit correspondre à la version Hibernate que vous utilisez. Je ne sais pas quelle version vous utilisez dans votre projet, mais la liste complète des correctifs fournis est disponible sur la page wiki du github du projet . La raison pour laquelle le correctif est différent pour les différentes versions d'Hibernate est que les créateurs d'Hibernate ont changé l'API plusieurs fois entre les versions.

En interne, le correctif utilise des indices de divestoclimb, Shane et quelques autres sources afin de créer une personnalisation UtcDateType. Ensuite, il mappe la norme java.util.Dateavec la coutume UtcDateTypequi gère toute la gestion du fuseau horaire nécessaire. Le mappage des types est réalisé en utilisant l' @Typedefannotation dans le package-info.javafichier fourni .

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

Vous pouvez trouver un article ici qui explique pourquoi un tel décalage temporel se produit et quelles sont les approches pour le résoudre.

orim
la source
1

Hibernate ne permet pas de spécifier des fuseaux horaires par annotation ou par tout autre moyen. Si vous utilisez Calendar au lieu de date, vous pouvez implémenter une solution de contournement à l'aide de la propriété HIbernate AccessType et implémenter vous-même le mappage. La solution la plus avancée consiste à implémenter un UserType personnalisé pour mapper votre date ou votre calendrier. Les deux solutions sont expliquées dans mon article de blog ici: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

Joobik
la source