114 votes

Comment stocker les dates/heures et les horodatages dans le fuseau horaire UTC avec JPA et Hibernate ?

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 2008-Feb-03 9:30am Pacific Standard Time (PST), je veux que l'heure UTC de 2008-Feb-03 5:30pm 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, 530pm est 530pm UTC. Lorsqu'elle sera affichée, elle sera formatée en tant que 9:30am PST.

1 votes

La réponse de Vlad Mihalcea fournit une réponse mise à jour (pour Hibernate 5.2+)

54voto

mitchnull Points 2950

À ma connaissance, vous devez placer l'ensemble de votre application Java dans le fuseau horaire UTC (afin qu'Hibernate stocke les dates en UTC), et vous devrez convertir ce fuseau horaire lorsque vous afficherez des données (c'est du moins ce que nous faisons).

Au démarrage, nous le faisons :

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

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

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

6 votes

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. cf. stackoverflow.com/questions/4123534/ .

2 votes

Mais si je démarre mon application en informant la JVM de la propriété "-Duser.timezone=+00:00", le comportement n'est-il pas le même ?

8 votes

Pour autant que je sache, cela fonctionnera dans tous les cas, sauf lorsque la JVM et le serveur de base de données se trouvent dans des fuseaux horaires différents.

47voto

divestoclimb Points 403

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

J'ai trouvé une solution à ce problème dans Hibernate 3.5 en sous-classant les éléments suivants org.hibernate.type.TimestampType qui force ces méthodes JDBC à utiliser l'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 pour chaque champ Date dans vos POJO (ce qui rompt é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 modifié l'API des types. Dans 3.6, j'ai écrit une classe UtcTimestampTypeDescriptor pour implémenter ceci.

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

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

3 votes

Comment dites-vous à Hibernate d'utiliser votre fichier personnalisé UtcTimestampType ?

0 votes

Divestoclimb, avec quelle version de Hibernate est UtcTimestampType compatible ?

2 votes

"ResultSet.getTimestamp et PreparedStatement.setTimestamp indiquent tous deux dans leur docs qu'ils transforment les dates en/depuis le fuseau horaire actuel de la JVM 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 de Java 6 pour ces méthodes. D'après stackoverflow.com/questions/4123534/ comment ces méthodes appliquent le fuseau horaire à une situation donnée. Date o Timestamp dépend du pilote JDBC.

11voto

Shane Points 1142

Ajouter une réponse qui est complètement basé sur et redevable à divestoclimb avec une touche de Shaun Stone. Je voulais juste l'expliquer en détail car c'est un problème courant et la solution est un peu confuse.

J'utilise Hibernate 4.1.4.Final, mais je pense que tout ce qui est postérieur à la version 3.6 fonctionnera.

Tout d'abord, créez le descripteur 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 qui autrement délègue 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 en tant que remplacement de type :

configuration.registerTypeOverride(new UtcTimestampType());

Maintenant, les timestamps ne devraient pas être concernés par le fuseau horaire de la JVM lors de leurs déplacements vers et depuis la base de données. HTH.

6 votes

Ce serait formidable de voir la solution pour JPA et Spring Configuration.

2 votes

Une remarque concernant l'utilisation de cette approche avec les requêtes natives dans Hibernate. Pour que ces types surchargés soient utilisé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 cette dernière option, Hibernate utilisera ses implémentations de type originales, car elles sont codées en dur.

0 votes

Où dois-je appeler l'instruction configuration.registerTypeOverride(new UtcTimestampType()) ; ?

10voto

joekutner Points 1037

On pourrait penser que ce problème courant est pris en charge par Hibernate. Mais ce n'est pas le cas ! Il existe quelques "trucs" pour y remédier.

La méthode que j'utilise consiste à stocker la date en tant que long dans la base de données. Je travaille donc toujours avec des millisecondes après le 1/1/70. J'ai ensuite des getters et setters sur ma classe qui renvoient/acceptent uniquement des dates. L'API reste donc la même. L'inconvénient est que j'ai des longs dans la base de données. Donc, avec SQL, je ne peux pratiquement faire que des comparaisons <,>,= - pas d'opérateurs de date fantaisistes.

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

Je pense que la manière correcte de traiter ce problème est d'utiliser un calendrier au lieu d'une date. Avec le calendrier, vous pouvez définir le TimeZone avant de persister.

NOTE : L'idiot de stackoverflow ne me laisse pas commenter, alors voici une réponse à David A.

Si vous créez cet objet à Chicago :

new Date(0);

Hibernate la fait persister sous la forme "12/31/1969 18:00:00". Les dates devraient être dépourvues de fuseau horaire, donc je ne vois pas pourquoi l'ajustement serait fait.

1 votes

Honte à moi ! Vous aviez raison et le lien de votre post l'explique bien. Maintenant je suppose que ma réponse mérite une réputation négative :)

1 votes

Pas du tout. Vous m'avez encouragé à poster un exemple très explicite de la raison pour laquelle c'est un problème.

4 votes

J'ai pu faire persister les heures correctement en utilisant l'objet Calendrier afin qu'elles soient stockées dans la base de données en tant que UTC comme vous l'avez suggéré. Cependant, lors de la lecture des entités persistées dans la base de données, Hibernate suppose qu'elles sont dans le fuseau horaire local et l'objet Calendrier est incorrect !

8voto

Moa Points 51

Il y a plusieurs fuseaux horaires en vigueur ici :

  1. Les classes Date de Java (util et sql), qui ont des fuseaux horaires implicites de UTC
  2. Le fuseau horaire dans lequel votre JVM fonctionne, et
  3. le fuseau horaire par défaut de votre serveur de base de données.

Tous ces éléments peuvent être différents. Hibernate/JPA présente un grave défaut de conception en ce sens qu'un utilisateur ne peut pas facilement s'assurer que les informations relatives au fuseau horaire sont préservées dans le serveur de base de données (ce qui permet de reconstruire les heures et les dates correctes dans la JVM).

Sans la possibilité de stocker (facilement) le fuseau horaire à l'aide de JPA/Hibernate, l'information est perdue et une fois l'information perdue, il devient coûteux de la reconstruire (si possible).

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

Désolé, cet article ne propose pas de solution de contournement (la réponse a déjà été donnée ailleurs), mais il explique pourquoi il est important de toujours conserver les informations relatives au fuseau horaire. Malheureusement, il semble que de nombreux informaticiens et praticiens de la programmation s'opposent à la nécessité des fuseaux horaires simplement parce qu'ils n'apprécient pas la perspective de "perte d'informations" et la façon dont cela rend des choses comme l'internationalisation très difficiles - ce qui est très important de nos jours avec les sites Web accessibles par les clients et les personnes de votre organisation qui se déplacent dans le monde entier.

1 votes

"Hibernate/JPA présente un grave défaut de conception" Je dirais que c'est un défaut de SQL, qui a traditionnellement permis que le fuseau horaire soit implicite, et donc potentiellement n'importe quoi. Stupide SQL.

3 votes

En fait, au lieu de toujours stocker le fuseau horaire, vous pouvez aussi standardiser 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 non plus en charge cette méthode directement :-/.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X