Cómo almacenar la fecha / hora y las marcas de tiempo en la zona horaria UTC con JPA e Hibernate

106

¿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 2008-Feb-03 9:30 am hora estándar del Pacífico (PST), entonces quiero que la hora UTC del 2008-Feb-03 5:30 pm 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 pm son las 530 pm UTC. Cuando se muestre, tendrá el formato de las 9:30 am PST.

Steve Kuo
fuente
1
La respuesta de Vlad Mihalcea proporciona una respuesta actualizada (para Hibernate 5.2+)
Dinei

Respuestas:

73

Con Hibernate 5.2, ahora puede forzar la zona horaria UTC usando la siguiente propiedad de configuración:

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

Para obtener más detalles, consulte este artículo .

Vlad Mihalcea
fuente
19
También escribí eso: D Ahora, ¿adivinen quién agregó soporte para esta función en Hibernate?
Vlad Mihalcea
Oh, justo ahora me di cuenta de que el nombre y la imagen son los mismos en tu perfil y esos artículos ... Buen trabajo Vlad :)
Dinei
@VladMihalcea si es para Mysql, es necesario decirle a MySql que use la zona horaria useTimezone=trueen la cadena de conexión. Entonces única propiedad establecer hibernate.jdbc.time_zonetrabajará
TheCoder
En realidad, debe establecerlo useLegacyDatetimeCodeen falso
Vlad Mihalcea
2
hibernate.jdbc.time_zone parece ignorarse o no tiene efecto cuando se usa con PostgreSQL
Alex R
48

Hasta donde yo sé, debe poner toda su aplicación Java en la zona horaria UTC (para que Hibernate almacene las fechas en UTC), y deberá convertir a la zona horaria que desee cuando muestre cosas (al menos lo hacemos de esta manera).

Al inicio, hacemos:

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

Y establezca la zona horaria deseada en DateFormat:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
mitchnull
fuente
5
mitchnull, su solución no funcionará en todos los casos porque Hibernate delega la configuración de fechas en el controlador JDBC y cada controlador JDBC maneja fechas y zonas horarias de manera diferente. consulte stackoverflow.com/questions/4123534/… .
Derek Mahar
2
Pero si inicio mi aplicación informando a la propiedad JVM "-Duser.timezone = + 00: 00", ¿no se comporta lo mismo?
rafa.ferreira
8
Por lo que puedo decir, funcionará en todos los casos, excepto cuando la JVM y el servidor de la base de datos se encuentren en diferentes zonas horarias.
Shane
stevekuo y @mitchnull ven la solución divestoclimb a continuación, que es mucho mejor y a prueba de efectos secundarios stackoverflow.com/a/3430957/233906
Cerber
¿HibernateJPA admite la anotación "@Factory" y "@Externalizer", así es como hago el manejo de utc de fecha y hora en la biblioteca OpenJPA. stackoverflow.com/questions/10819862/…
Whome
44

Hibernate ignora las cosas de la zona horaria en Dates (porque no hay ninguna), pero en realidad es la capa JDBC la que está causando problemas. ResultSet.getTimestampy PreparedStatement.setTimestampambos dicen en sus documentos que transforman fechas a / desde la zona horaria actual de JVM de forma predeterminada al leer y escribir desde / a la base de datos.

Se me ocurrió una solución a esto en Hibernate 3.5 mediante la subclasificación org.hibernate.type.TimestampTypeque 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 sepa de 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 inicia la aplicación, si configura TimestampTypeDescriptor.INSTANCE en una instancia de UtcTimestampTypeDescriptor, todas las marcas de tiempo se almacenarán y se tratarán como si estuvieran en UTC sin tener que cambiar las anotaciones en los POJO. [No lo he probado todavía]

bucear
fuente
3
¿Cómo le dices a Hibernate que use tu costumbre UtcTimestampType?
Derek Mahar
divestoclimb, ¿con qué versión de Hibernate es UtcTimestampTypecompatible?
Derek Mahar
2
"ResultSet.getTimestamp y PreparedStatement.setTimestamp dicen en sus documentos que transforman las fechas hacia / desde la zona horaria actual de JVM de forma predeterminada al leer y escribir desde / hacia la base de datos". Tiene una referencia? No veo ninguna mención de esto en Java 6 Javadocs para estos métodos. Según stackoverflow.com/questions/4123534/… , cómo estos métodos aplican la zona horaria a un determinado Dateo Timestampdepende del controlador JDBC.
Derek Mahar
1
Podría haber jurado que leí eso sobre el uso de la zona horaria de JVM el año pasado, pero ahora no puedo encontrarlo. Podría haberlo encontrado en los documentos para un controlador JDBC específico y generalizado.
Divestoclimb
2
Para que funcione la versión 3.6 de su ejemplo, tuve que crear un nuevo tipo que era básicamente un envoltorio alrededor del TimeStampType y luego establecer ese tipo en el campo.
Shaun Stone
17

Con Spring Boot JPA, use el siguiente código en su archivo application.properties y obviamente puede modificar la zona horaria a su elección

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

Luego, en su archivo de clase de entidad,

@Column
private LocalDateTime created;
JavaGeek
fuente
11

Agregando una respuesta que está completamente basada y en deuda con divestoclimb con una pista de Shaun Stone. Solo quería explicarlo en detalle, ya que es un problema común y la solución es un poco confusa.

Esto está usando Hibernate 4.1.4.Final, aunque sospecho que cualquier cosa después de 3.6 funcionará.

Primero, cree el 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 );
            }
        };
    }
}

Luego, cree UtcTimestampType, que usa UtcTimestampTypeDescriptor en lugar de TimestampTypeDescriptor como SqlTypeDescriptor en la llamada del superconstructor pero, de lo contrario, delega todo a 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);
    }
}

Finalmente, cuando inicialice su configuración de Hibernate, registre UtcTimestampType como una anulación de tipo:

configuration.registerTypeOverride(new UtcTimestampType());

Ahora, las marcas de tiempo no deben preocuparse por la zona horaria de la JVM en su camino hacia y desde la base de datos. HTH.

Shane
fuente
6
Sería genial ver la solución para JPA y Spring Configuration.
Berenjena
2
Una nota sobre el uso de este enfoque con consultas nativas dentro de Hibernate. Para que se utilicen estos tipos anulados, debe establecer el valor con query.setParameter (int pos, Object value), no query.setParameter (int pos, Date value, TemporalType temporalType). Si usa este último, Hibernate usará sus implementaciones de tipo original, ya que están codificadas de forma rígida.
Nigel
¿Dónde debo llamar a la instrucción configuration.registerTypeOverride (new UtcTimestampType ()); ?
Stony
@Stony Dondequiera que inicialices tu configuración de Hibernate. Si tiene un HibernateUtil (la mayoría lo tiene), estará allí.
Shane
1
Funciona, pero después de comprobarlo me di cuenta de que no es necesario para que el servidor de Postgres funcione en timezone = "UTC" y con todos los tipos predeterminados de marcas de tiempo como "marca de tiempo con zona horaria" (se hace automáticamente). Pero, aquí hay una versión fija para Hibernate 4.3.5 GA como una clase única completa y anulando el frijol de fábrica de primavera pastebin.com/tT4ACXn6
Lukasz Frankowski
10

Usted pensaría que Hibernate se ocuparía de este problema común. ¡Pero no lo es! Hay algunos "trucos" para hacerlo bien.

El que yo uso es para almacenar la fecha como Long en la base de datos. Entonces siempre estoy trabajando con milisegundos después del 1/1/70. Luego tengo getters y setters en mi Clase que devuelven / aceptan solo Fechas. Entonces la API sigue siendo la misma. El lado negativo es que tengo largos en la base de datos. Entonces, con SQL prácticamente solo puedo hacer <,>, = comparaciones, no operadores de fecha sofisticados.

Otro enfoque es utilizar un tipo de mapeo personalizado como se describe aquí: http://www.hibernate.org/100.html

Sin embargo, creo que la forma correcta de lidiar con esto es usar un calendario en lugar de una fecha. Con el calendario puede configurar la zona horaria antes de continuar.

NOTA: Stackoverflow tonto no me deja comentar, así que aquí hay una respuesta a david a.

Si crea este objeto en Chicago:

new Date(0);

Hibernate lo persiste como "31/12/1969 18:00:00". Las fechas deben estar desprovistas de zona horaria, por lo que no estoy seguro de por qué se haría el ajuste.

codefinger
fuente
1
¡Me avergüenza! Tenías razón y el enlace de tu publicación lo explica bien. Ahora supongo que mi respuesta merece una reputación negativa :)
david a.
1
De ningún modo. me animaste a publicar un ejemplo muy explícito de por qué esto es un problema.
codefinger
4
Pude conservar los tiempos correctamente utilizando el objeto Calendario para que se almacenen en la base de datos como UTC como sugirió. Sin embargo, al leer las entidades persistentes de la base de datos, Hibernate asume que están en la zona horaria local y que el objeto Calendario es incorrecto.
John K
1
John K, para resolver este Calendarproblema de lectura, creo que Hibernate o JPA deberían proporcionar alguna forma de especificar, para cada asignación, la zona horaria a la que Hibernate debería traducir la fecha en la que lee y escribe en una TIMESTAMPcolumna.
Derek Mahar
joekutner, después de leer stackoverflow.com/questions/4123534/… , he venido a compartir su opinión de que deberíamos almacenar milisegundos desde la Epoch en la base de datos en lugar de a, Timestampya que no necesariamente podemos confiar en el controlador JDBC para almacenar fechas como esperaríamos.
Derek Mahar
8

Hay varias zonas horarias en funcionamiento aquí:

  1. Clases de fecha de Java (util y sql), que tienen zonas horarias implícitas de UTC
  2. La zona horaria en la que se ejecuta su JVM y
  3. la zona horaria predeterminada de su servidor de base de datos.

Todos estos pueden ser diferentes. Hibernate / JPA tiene una deficiencia de diseño severa en el sentido de que un usuario no puede garantizar fácilmente que la información de la zona horaria se conserve en el servidor de la base de datos (lo que permite la reconstrucción de las horas y fechas correctas en la JVM).

Sin la capacidad de almacenar (fácilmente) la zona horaria usando JPA / Hibernate, la información se pierde y una vez que se pierde la información se vuelve costoso construirla (si es posible).

Yo diría que es mejor almacenar siempre la información de la zona horaria (debería ser la predeterminada) y los usuarios deberían tener la capacidad opcional de optimizar la zona horaria (aunque solo afecta la visualización, todavía hay una zona horaria implícita en cualquier fecha).

Lo sentimos, esta publicación no proporciona una solución alternativa (que se ha respondido en otro lugar), pero es una racionalización de por qué siempre es importante almacenar información de zona horaria. Desafortunadamente, parece que muchos informáticos y profesionales de la programación argumentan en contra de la necesidad de zonas horarias simplemente porque no aprecian la perspectiva de la "pérdida de información" y cómo eso hace que cosas como la internacionalización sean muy difíciles, lo cual es muy importante en estos días con sitios web accesibles por clientes y personas de su organización a medida que se mueven por el mundo.

Moa
fuente
1
"Hibernate / JPA tiene una deficiencia de diseño severa" Yo diría que es una deficiencia en SQL, que tradicionalmente ha permitido que la zona horaria sea implícita y, por lo tanto, potencialmente cualquier cosa. SQL tonto.
Raedwald
3
En realidad, en lugar de almacenar siempre la zona horaria, también puede estandarizar en una zona horaria (generalmente UTC) y convertir todo a esta zona horaria cuando persiste (y viceversa cuando lee). Esto es lo que solemos hacer. Sin embargo, JDBC tampoco lo admite directamente: - /.
2012
3

Por favor, eche un vistazo a mi proyecto en Sourceforge, que tiene tipos de usuario para los tipos estándar de fecha y hora de SQL, así como JSR 310 y Joda Time. Todos los tipos intentan abordar el problema de la compensación. Ver http://sourceforge.net/projects/usertype/

EDITAR: En respuesta a la pregunta de Derek Mahar adjunta a este comentario:

"Chris, ¿tus tipos de usuario funcionan con Hibernate 3 o superior? - Derek Mahar 7 de noviembre de 2010 a las 12:30"

Sí, estos tipos son compatibles con las versiones de Hibernate 3.x, incluido Hibernate 3.6.

Chris Pheby
fuente
2

La fecha no está en ninguna zona horaria (es una oficina de milisegundos desde un momento definido en el tiempo, lo mismo para todos), pero las bases de datos (R) subyacentes generalmente almacenan marcas de tiempo en formato político (año, mes, día, hora, minuto, segundo,. ..) que es sensible a la zona horaria.

Para ser serio, Hibernate DEBE permitir que se le diga dentro de alguna forma de mapeo que la fecha de la base de datos está en tal o cual zona horaria para que cuando la cargue o la almacene no asuma la suya propia ...


fuente
1

Encontré el mismo problema cuando quería almacenar las fechas en la base de datos como UTC y evitar el uso varchary String <-> java.util.Dateconversiones explícitas , o configurar toda mi aplicación Java en la zona horaria UTC (porque esto podría conducir a otros problemas inesperados, si la JVM es compartido en muchas aplicaciones).

Por lo tanto, existe un proyecto de código abierto DbAssist, que le permite corregir fácilmente la lectura / escritura como fecha UTC de la base de datos. Dado que está utilizando JPA Annotations para mapear los campos en la entidad, todo lo que tiene que hacer es incluir la siguiente dependencia en su pomarchivo Maven :

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

Luego aplica la corrección (para el ejemplo de Hibernate + Spring Boot) agregando una @EnableAutoConfigurationanotación antes de la clase de aplicación Spring. Para obtener instrucciones de instalación de otras configuraciones y más ejemplos de uso, solo consulte el github del proyecto .

Lo bueno es que no tienes que modificar las entidades en absoluto; puede dejar sus java.util.Datecampos como están.

5.2.2tiene que corresponder a la versión de Hibernate que está utilizando. No estoy seguro de qué versión está utilizando en su proyecto, pero la lista completa de correcciones proporcionadas está disponible en la página wiki del github del proyecto . La razón por la que la solución es diferente para varias versiones de Hibernate es porque los creadores de Hibernate cambiaron la API un par de veces entre las versiones.

Internamente, la solución utiliza sugerencias de divestoclimb, Shane y algunas otras fuentes para crear un archivo personalizado UtcDateType. Luego mapea el estándar java.util.Datecon el personalizado UtcDateTypeque maneja todo el manejo necesario de la zona horaria. El mapeo de los tipos se logra mediante la @Typedefanotación en el package-info.javaarchivo proporcionado .

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

Puede encontrar un artículo aquí que explica por qué ocurre tal cambio de tiempo y cuáles son los enfoques para resolverlo.

orim
fuente
1

Hibernate no permite especificar zonas horarias mediante anotaciones o cualquier otro medio. Si usa Calendar en lugar de date, puede implementar una solución alternativa usando la propiedad AccessType de HIbernate e implementando el mapeo usted mismo. La solución más avanzada es implementar un UserType personalizado para mapear su fecha o calendario. Ambas soluciones se explican en la publicación de mi blog aquí: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

joobik
fuente