Comment configurer JPA/Hibernate pour stocker une date/heure dans la base de données en tant que fuseau horaire UTC (GMT)? Considérons 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 3 février 2008 à 9 h 30, heure normale du Pacifique, je souhaite que l’heure UTC du 3 février 2008 à 17h30 soit stockée dans la base de données. De même, lorsque la date est extraite de la base de données, je souhaite qu’elle soit interprétée en tant que UTC. Donc, dans ce cas, 17h30 est 17h30 UTC. Lorsqu'il est affiché, il est formaté à 9h30, heure du Pacifique.
Avec Hibernate 5.2, vous pouvez maintenant 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 .
Autant que je sache, vous devez placer l'intégralité de votre Java dans le fuseau horaire UTC (pour qu'Hibernate enregistre les dates au format UTC)), et vous devrez convertir le fuseau horaire souhaité vous affichez des choses (au moins nous le faisons de cette façon).
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 fuseaux horaires dans Dates (car il n'y en a pas), mais c'est en réalité la couche JDBC qui pose problème. ResultSet.getTimestamp
et PreparedStatement.setTimestamp
_ les deux disent dans leur documentation qu'ils transforment par défaut les dates du fuseau horaire actuel de la machine virtuelle Java 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-classant org.hibernate.type.TimestampType
qui oblige ces méthodes JDBC à utiliser le temps 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 devrait être faite pour réparer 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 de chaque champ Date de vos POJO (et rompt également la compatibilité JPA pure), à moins que quelqu'un ne connaisse une méthode de remplacement plus générale.
UPDATE: Hibernate 3.6 a modifié les types d'API. En 3.6, j'ai écrit une classe UtcTimestampTypeDescriptor pour implémenter cela.
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 au format UTC sans qu'il soit nécessaire de modifier les annotations sur les POJO. [Je n'ai pas encore testé cela]
Ajouter une réponse qui est complètement basée sur et redevable de divestoclimb avec un soupçon de Shaun Stone. Je voulais juste expliquer en détail car c'est un problème commun et la solution est un peu déroutant.
Ceci utilise Hibernate 4.1.4.Final, bien que je pense que quoi que ce soit après 3.6 fonctionnera.
Tout d'abord, créez 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 );
}
};
}
}
Créez ensuite UtcTimestampType, qui utilise UtcTimestampTypeDescriptor au lieu de TimestampTypeDescriptor en tant que SqlTypeDescriptor dans l’appel du super constructeur, mais autrement, il 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 substitution de type:
configuration.registerTypeOverride(new UtcTimestampType());
À présent, les horodatages ne doivent pas concerner le fuseau horaire de la machine virtuelle lors de son trajet vers et depuis la base de données. HTH.
On pourrait penser que ce problème courant serait traité par Hibernate. Mais ce n'est pas! Il y a quelques "bidouilles" pour bien faire les choses.
Celui que j'utilise est de stocker la date en tant que long dans la base de données. Donc, je travaille toujours avec millisecondes après 1/1/70. J'ai alors des getters et des setters sur ma classe qui retournent/acceptent uniquement les dates. Donc, 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 <,>, = - et non des opérateurs de date sophistiqués.
Une autre approche consiste à utiliser un type de mappage personnalisé tel que décrit ici: http://www.hibernate.org/100.html
Je pense que la bonne façon de gérer cela consiste à utiliser un calendrier au lieu d'une date. Avec le calendrier, vous pouvez définir le fuseau horaire avant de continuer.
NOTE: Silly stackoverflow ne me laisse pas commenter, alors voici une réponse à David a.
Si vous créez cet objet à Chicago:
new Date(0);
Hibernate persiste sous le nom "31/12/1969 18:00:00". Les dates doivent être dépourvues de fuseau horaire, je ne suis donc pas certain du pourquoi de cet ajustement.
Il y a plusieurs fuseaux horaires en fonctionnement ici:
Tout cela peut être différent. Hibernate/JPA souffre d'un grave défaut de conception, car un utilisateur ne peut pas facilement garantir la conservation des informations de fuseau horaire dans le serveur de base de données (ce qui permet de reconstituer les heures et les dates correctes dans la JVM).
Sans la possibilité de stocker (facilement) un fuseau horaire à l'aide de JPA/Hibernate, les informations sont perdues et, une fois ces informations perdues, il devient coûteux de les construire (dans la mesure du 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 puissent alors avoir la possibilité facultative d'optimiser le fuseau horaire (bien que cela n'affecte réellement que l'affichage, il existe toujours un fuseau horaire implicite pour toute date).
Désolé, ce message ne fournit pas de solution de contournement (il a été répondu ailleurs), mais il explique de manière rationnelle pourquoi il est important de toujours stocker les informations de 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 complication qui rend l’internationalisation très difficile - 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.
Avec Spring Boot JPA, utilisez le code ci-dessous dans votre fichier application.properties et vous pouvez évidemment modifier le fuseau horaire de votre choix.
spring.jpa.properties.hibernate.jdbc.time_zone = UTC
Puis dans votre fichier de classe Entity,
@Column
private LocalDateTime created;
Veuillez jeter un coup d’œil à mon projet sur Sourceforge, qui contient des types d’utilisateur 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 supérieur? - Derek Mahar le 7 novembre 10 à 12h30"
Oui, ces types prennent en charge les versions d'Hibernate 3.x, y compris Hibernate 3.6.
La date ne se trouve dans aucun fuseau horaire (c’est un bureau de la milliseconde à partir d’un moment défini, même 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 permettre à être dit 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 charge ou le stocke il n'assume pas le sien ...
Hibernate ne permet pas de spécifier des fuseaux horaires par annotation ou 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 en implémentant 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 cet article de blog: http://dev-metal.blogspot.com/2010/11/mapping-dates-and-time-zones-with.html
J'ai rencontré exactement le même problème lorsque je souhaitais stocker les dates dans la base de données au format UTC et éviter d'utiliser les conversions varchar
et explicites String <-> Java.util.Date
, Ou de paramétrer mon ensemble Java = app dans le fuseau horaire UTC (car cela pourrait entraîner d'autres problèmes inattendus si la machine virtuelle est partagée entre plusieurs applications).
Il existe donc un projet open source DbAssist
, qui vous permet de corriger facilement les dates de lecture/écriture au format UTC à partir de la base de données. Puisque vous utilisez JPA Annotations pour mapper les champs de l'entité, il vous suffit d'inclure la dépendance suivante à votre fichier Maven pom
:
<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 l'annotation @EnableAutoConfiguration
Avant la classe d'application Spring. Pour d'autres instructions d'installation et d'autres exemples d'utilisation, référez-vous simplement à github du projet.
La bonne chose est que vous n'avez pas du tout besoin de modifier les entités; 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 projet github . La raison pour laquelle le correctif est différent pour les 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 astuces de divestoclimb, Shane et quelques autres sources afin de créer un UtcDateType
personnalisé. Ensuite, il mappe le standard Java.util.Date
Avec le personnalisé UtcDateType
qui gère tous les traitements de fuseau horaire nécessaires. 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 dans le temps se produit et quelles sont les approches pour le résoudre.