Cómo almacenar fecha/hora y marcas de tiempo en la zona horaria UTC con JPA e Hibernate
¿Cómo puedo configurar JPA/Hibernate para almacenar una fecha/hora en la base de datos como zona horaria UTC (GMT)? Considere esta entidad JPA anotada:
public class Event {
@Id
public int id;
@Temporal(TemporalType.TIMESTAMP)
public java.util.Date date;
}
Si la fecha es 03 de febrero de 2008 a las 9:30 a. m., hora estándar del Pacífico (PST), entonces quiero que la hora UTC del 3 de febrero de 2008 a las 5:30 p. m. se almacene en la base de datos. Del mismo modo, cuando se recupera la fecha de la base de datos, quiero que se interprete como UTC. Entonces, en este caso, las 530 p. m. son las 530 p. m. UTC. Cuando se muestre, tendrá el formato de 9:30 a. m. PST.
Desde Hibernate 5.2, ahora puede forzar la zona horaria UTC agregando la siguiente propiedad de configuración en el properties.xml
archivo de configuración JPA:
<property name="hibernate.jdbc.time_zone" value="UTC"/>
Si está utilizando Spring Boot, agregue esta propiedad a su application.properties
archivo:
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
Hasta donde yo sé, debes colocar toda tu aplicación Java en la zona horaria UTC (para que Hibernate almacene las fechas en UTC), y necesitarás convertirla a cualquier zona horaria deseada cuando muestres cosas (al menos nosotros lo hacemos). Por aquí).
Al inicio, hacemos:
TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
Y configure la zona horaria deseada en DateFormat:
fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
Hibernate ignora las zonas horarias en Fechas (porque no hay ninguna), pero en realidad es la capa JDBC la que está causando problemas. ResultSet.getTimestamp
y PreparedStatement.setTimestamp
ambos dicen en sus documentos que transforman fechas hacia/desde la zona horaria actual de JVM de forma predeterminada al leer y escribir desde/hacia la base de datos.
Se me ocurrió una solución a esto en Hibernate 3.5 mediante una subclase org.hibernate.type.TimestampType
que obliga a estos métodos JDBC a usar UTC en lugar de la zona horaria 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));
}
}
Se debe hacer lo mismo para corregir TimeType y DateType si usa esos tipos. La desventaja es que tendrá que especificar manualmente que estos tipos se usarán en lugar de los valores predeterminados en cada campo de Fecha en sus POJO (y también rompe la compatibilidad pura con JPA), a menos que alguien conozca un método de anulación más general.
ACTUALIZACIÓN: Hibernate 3.6 ha cambiado los tipos de API. En 3.6, escribí una clase UtcTimestampTypeDescriptor para implementar esto.
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 );
}
};
}
}
Ahora, cuando se inicie la aplicación, si configura TimestampTypeDescriptor.INSTANCE en una instancia de UtcTimestampTypeDescriptor, todas las marcas de tiempo se almacenarán y tratarán como si estuvieran en UTC sin tener que cambiar las anotaciones en los POJO. [No he probado esto todavía]