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

Comment configurer JPA / Hibernate pour stocker une date / heure dans la firebase database 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-fév-03 09:30 heure normale du Pacifique (PST), alors je veux que l’heure UTC 2008-fév-03 17:30 soit enregistrée dans la firebase database. De même, lorsque la date est extraite de la firebase database, je veux qu’elle soit interprétée comme UTC. Donc, dans ce cas, 17h30 est 17h30 UTC. Quand il est affiché, il sera formaté à 9h30 heure du Pacifique.

Avec Hibernate 5.2, vous pouvez maintenant forcer le fuseau horaire UTC en utilisant la propriété de configuration suivante:

  

Pour plus de détails, consultez cet article .

Au meilleur de ma connaissance, vous devez placer l’intégralité de votre application Java dans le fuseau horaire UTC (pour que Hibernate stocke les dates en UTC), et vous aurez besoin de convertir le fuseau horaire souhaité lorsque vous affichez des éléments (au moins 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")) 

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 problème. ResultSet.getTimestamp et PreparedStatement.setTimestamp indiquent tous deux dans leurs documents qu’ils transforment par défaut les dates du fuseau horaire actuel de la JVM lors de la lecture et de l’écriture depuis / vers la firebase database.

J’ai trouvé une solution à cela dans Hibernate 3.5 en sous- org.hibernate.type.TimestampType qui force 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, Ssortingng 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 devrait ê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 dans chaque champ Date de vos POJO (et également ne pas respecter la compatibilité JPA pure), à ​​moins que vous connaissiez une méthode de remplacement plus générale.

MISE À JOUR: Hibernate 3.6 a changé l’API de 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  ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder( 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  ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicExtractor( javaTypeDescriptor, this ) { @Override protected X doExtract(ResultSet rs, Ssortingng 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 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]

Ajouter une réponse qui est entièrement basée sur et divutable de divestoclimb avec un indice de Shaun Stone. Je voulais juste l’épeler 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 quelque chose après 3.6 fonctionnera.

Tout d’abord, créez le UtcTimestampTypeDescriptor de divestoclimb

 public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor { public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor(); private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); public  ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder( 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  ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicExtractor( javaTypeDescriptor, this ) { @Override protected X doExtract(ResultSet rs, Ssortingng 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 le tout à TimestampType:

 public class UtcTimestampType extends AbstractSingleColumnStandardBasicType implements VersionType, LiteralType { public static final UtcTimestampType INSTANCE = new UtcTimestampType(); public UtcTimestampType() { super( UtcTimestampTypeDescriptor.INSTANCE, JdbcTimestampTypeDescriptor.INSTANCE ); } public Ssortingng getName() { return TimestampType.INSTANCE.getName(); } @Override public Ssortingng[] 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 getComparator() { return TimestampType.INSTANCE.getComparator(); } public Ssortingng objectToSQLSsortingng(Date value, Dialect dialect) throws Exception { return TimestampType.INSTANCE.objectToSQLSsortingng(value, dialect); } public Date fromSsortingngValue(Ssortingng xml) throws HibernateException { return TimestampType.INSTANCE.fromSsortingngValue(xml); } } 

Enfin, lorsque vous initialisez votre configuration Hibernate, enregistrez UtcTimestampType en tant que remplacement de type:

 configuration.registerTypeOverride(new UtcTimestampType()); 

Maintenant, les horodatages ne doivent pas concerner le fuseau horaire de la JVM sur leur chemin vers et depuis la firebase database. HTH.

Vous penseriez que Hibernate s’occuperait de ce problème commun. Mais ce n’est pas! Il y a quelques “hacks” pour bien faire les choses.

Celui que j’utilise consiste à stocker la date en tant que long dans la firebase database. Je travaille donc toujours en millisecondes après le 1/1/70. J’ai alors des getters et des setters sur ma classe qui renvoient / acceptent uniquement les dates. Donc, l’API rest la même. L’inconvénient est que j’ai de longs dans la firebase database. Donc, 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.

NOTE: Stupide stackoverflow ne me laissera pas de commentaire, alors voici une réponse à David a.

Si vous créez cet object dans Chicago:

 new Date(0); 

Hibernate le persiste sous la forme “31/12/1969 18:00:00”. Les dates doivent être dépourvues de fuseau horaire, alors je ne suis pas sûr de savoir pourquoi l’ajustement sera effectué.

Il y a plusieurs fuseaux horaires en fonctionnement ici:

  1. Les 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 firebase database.

Tout cela peut être différent. Hibernate / JPA présente une grave déficience de conception en ce sens qu’un utilisateur ne peut pas s’assurer facilement que les informations de fuseau horaire sont conservées sur le serveur de firebase database (ce qui permet de reconstituer les heures et les 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 les utilisateurs devraient alors avoir la possibilité d’optimiser le fuseau horaire (bien que cela affecte uniquement l’affichage, il existe toujours un fuseau horaire).

Désolé, cet article ne fournit pas de solution (cela a déjà été répondu ailleurs), mais c’est une raison pour laquelle il est important de toujours stocker les informations sur le fuseau horaire. Malheureusement, il semble que de nombreux informaticiens et praticiens de la programmation contestent le besoin de fuseaux horaires simplement parce qu’ils n’apprécient pas la perspective de la «perte d’informations» et que cela rend l’internationalisation très difficile. les clients et les personnes de votre organisation qui se déplacent à travers le monde.

S’il vous plaît jeter un oeil à mon projet sur Sourceforge qui a des types d’utilisateurs pour les types standard de date et d’heure SQL 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 utilisateurs utilisent Hibernate 3 ou supérieur? – Derek Mahar 7 novembre 10 à 12h30”

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

Date n’est pas dans un fuseau horaire (c’est un milliseconde de bureau à partir d’un moment défini pour tout le monde), mais les DB sous-jacents stockent généralement des 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é à indiquer dans une certaine forme de mappage que la date de la firebase database se trouve dans tel ou tel fuseau horaire, de sorte que quand il la charge ou la stocke

Hibernate ne permet pas de spécifier des fuseaux horaires par annotation ou par tout autre moyen. Si vous utilisez le calendrier au lieu de la date, vous pouvez implémenter une solution de contournement à l’aide de la propriété HIbernate AccessType et implémenter le mappage vous-même. 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 cet article: http://dev-metal.blogspot.com/2010/11/mapping-dates-and-time-zones-with.html

J’ai rencontré le même problème lorsque je voulais stocker les dates dans la firebase database en UTC et éviter d’utiliser varchar et les conversions Ssortingng <-> java.util.Date explicites ou 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ée entre de nombreuses applications).

Il existe donc un projet open source, DbAssist , qui vous permet de corriger facilement la lecture / écriture en tant que date UTC à partir de la firebase database. Puisque vous utilisez les annotations JPA pour mapper les champs dans l’entité, il vous suffit d’inclure la dépendance suivante à votre fichier pom Maven:

  com.montrosesoftware DbAssist-5.2.2 1.0-RELEASE  

Ensuite, vous appliquez le correctif (pour l’exemple Hibernate + Spring Boot) en ajoutant @EnableAutoConfiguration annotation @EnableAutoConfiguration avant la classe d’application Spring. Pour les 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 à modifier les entités du tout; vous pouvez laisser leurs champs java.util.Date tels quels.

5.2.2 doit correspondre à la version d’Hibernate que vous utilisez. Je ne suis pas sûr de la version que 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 différentes versions d’Hibernate est que les créateurs d’Hibernate ont modifié 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 un UtcDateType personnalisé. Ensuite, il mappe le UtcDateType java.util.Date standard avec le type d’ UtcDateType personnalisé qui gère tout le traitement de fuseau horaire nécessaire. Le mappage des types est réalisé à l’aide de l’annotation @Typedef dans le fichier package-info.java 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 se produit du tout et quelles sont les approches pour le résoudre.