Como armazenar data / hora e carimbos de data / hora no fuso horário UTC com JPA e Hibernate

106

Como posso configurar JPA / Hibernate para armazenar uma data / hora no banco de dados como fuso horário UTC (GMT)? Considere esta entidade JPA anotada:

public class Event {
    @Id
    public int id;

    @Temporal(TemporalType.TIMESTAMP)
    public java.util.Date date;
}

Se a data for 03 de fevereiro de 2008, 9h30, horário padrão do Pacífico (PST), quero o horário UTC de 03 de fevereiro de 2008 às 17h30 armazenado no banco de dados. Da mesma forma, quando a data é recuperada do banco de dados, quero que seja interpretada como UTC. Portanto, neste caso, 17h30 são 17h30 UTC. Quando for exibido, será formatado como 9h30 PST.

Steve Kuo
fonte
1
A resposta de Vlad Mihalcea fornece uma resposta atualizada (para Hibernate 5.2+)
Dinei

Respostas:

73

Com o Hibernate 5.2, agora você pode forçar o fuso horário UTC usando a seguinte propriedade de configuração:

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

Para mais detalhes, confira este artigo .

Vlad Mihalcea
fonte
19
Eu escrevi esse também: D Agora, adivinhe quem adicionou suporte para esse recurso no Hibernate?
Vlad Mihalcea
Ah, só agora percebi que o nome e a foto são os mesmos em seu perfil e naqueles artigos ... Bom trabalho Vlad :)
Dinei
@VladMihalcea se for para o Mysql, é necessário dizer ao MySql para usar o fuso horário usando useTimezone=truena string de conexão. Então, apenas a configuração da propriedade hibernate.jdbc.time_zonefuncionará
TheCoder
Na verdade, você precisa definir useLegacyDatetimeCodecomo falso
Vlad Mihalcea
2
hibernate.jdbc.time_zone parece ser ignorado ou não tem efeito quando usado com PostgreSQL
Alex R
48

Pelo que eu sei, você precisa colocar todo o seu aplicativo Java no fuso horário UTC (para que o Hibernate armazene as datas em UTC), e você precisará converter para qualquer fuso horário desejado quando exibir as coisas (pelo menos nós fazemos isso deste jeito).

Na inicialização, fazemos:

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

E defina o fuso horário desejado para o DateFormat:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
mitchnull
fonte
5
mitchnull, sua solução não funcionará em todos os casos porque o Hibernate delega a configuração de datas para o driver JDBC e cada driver JDBC lida com datas e fusos horários de forma diferente. consulte stackoverflow.com/questions/4123534/… .
Derek Mahar de
2
Mas se eu iniciar meu aplicativo informando a propriedade "-Duser.timezone = + 00: 00" da JVM, o mesmo não se comporta?
rafa.ferreira
8
Até onde eu sei, funcionará em todos os casos, exceto quando a JVM e o servidor de banco de dados estiverem em fusos horários diferentes.
Shane,
stevekuo e @mitchnull veem a solução divestoclimb abaixo, que é muito mais melhor e à prova de efeitos colaterais stackoverflow.com/a/3430957/233906
Cerber
O HibernateJPA suporta a anotação "@Factory" e "@Externalizer", é como eu faço o manuseio de datetime utc na biblioteca OpenJPA. stackoverflow.com/questions/10819862/…
Quem é
44

O Hibernate ignora coisas de fuso horário em Datas (porque não existe), mas é na verdade a camada JDBC que está causando problemas. ResultSet.getTimestampe PreparedStatement.setTimestampambos dizem em seus documentos que transformam as datas de / para o fuso horário JVM atual por padrão ao ler e gravar de / para o banco de dados.

Eu encontrei uma solução para isso no Hibernate 3.5 criando uma subclasse org.hibernate.type.TimestampTypeque força esses métodos JDBC a usar UTC em vez do fuso horário 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));
    }
}

A mesma coisa deve ser feita para corrigir TimeType e DateType se você usar esses tipos. A desvantagem é que você terá que especificar manualmente que esses tipos devem ser usados ​​em vez dos padrões em todos os campos de Data em seus POJOs (e também quebra a compatibilidade JPA pura), a menos que alguém conheça um método de substituição mais geral.

ATUALIZAÇÃO: Hibernate 3.6 mudou a API de tipos. No 3.6, escrevi uma classe UtcTimestampTypeDescriptor para implementar isso.

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

Agora, quando o aplicativo é iniciado, se você definir TimestampTypeDescriptor.INSTANCE para uma instância de UtcTimestampTypeDescriptor, todos os carimbos de data / hora serão armazenados e tratados como UTC sem ter que alterar as anotações em POJOs. [Eu não testei isso ainda]

desinvestir
fonte
3
Como você diz ao Hibernate para usar seu custom UtcTimestampType?
Derek Mahar
divestoclimb, com qual versão do Hibernate é UtcTimestampTypecompatível?
Derek Mahar de
2
"ResultSet.getTimestamp e PreparedStatement.setTimestamp dizem em seus documentos que transformam datas de / para o fuso horário JVM atual por padrão ao ler e gravar de / para o banco de dados." Você tem uma referência? Não vejo nenhuma menção a isso nos Javadocs Java 6 para esses métodos. De acordo com stackoverflow.com/questions/4123534/… , como esses métodos aplicam o fuso horário a um determinado Dateou Timestampé dependente do driver JDBC.
Derek Mahar de
1
Eu poderia jurar que li sobre o uso do fuso horário da JVM no ano passado, mas agora não consigo encontrar. Eu posso ter encontrado na documentação de um driver JDBC específico e generalizado.
divestoclimb
2
Para fazer a versão 3.6 do seu exemplo funcionar, eu tive que criar um novo tipo que era basicamente um invólucro em torno do TimeStampType e então definir esse tipo no campo.
Shaun Stone
17

Com Spring Boot JPA, use o código abaixo em seu arquivo application.properties e, obviamente, você pode modificar o fuso horário de acordo com sua escolha

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

Então, em seu arquivo de classe Entity,

@Column
private LocalDateTime created;
JavaGeek
fonte
11

Adicionando uma resposta que é completamente baseada e devida ao desinvestimento com uma dica de Shaun Stone. Só queria explicar em detalhes, pois é um problema comum e a solução é um pouco confusa.

Isso está usando o Hibernate 4.1.4.Final, embora eu suspeite que qualquer coisa após o 3.6 funcionará.

Primeiro, crie 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 );
            }
        };
    }
}

Em seguida, crie UtcTimestampType, que usa UtcTimestampTypeDescriptor em vez de TimestampTypeDescriptor como SqlTypeDescriptor na chamada do superconstrutor, mas delega tudo para 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, ao inicializar a configuração do Hibernate, registre UtcTimestampType como uma substituição de tipo:

configuration.registerTypeOverride(new UtcTimestampType());

Agora, os timestamps não devem se preocupar com o fuso horário da JVM em seu caminho de ida e volta para o banco de dados. HTH.

Shane
fonte
6
Seria ótimo ver a solução para JPA e Spring Configuration.
Aubergine de
2
Uma nota sobre o uso dessa abordagem com consultas nativas dentro do Hibernate. Para que esses tipos substituídos sejam usados, você deve definir o valor com query.setParameter (int pos, Object value), não query.setParameter (int pos, Date value, TemporalType temporalType). Se você usar o último, o Hibernate usará suas implementações de tipo original, já que são codificadas.
Nigel de
Onde devo chamar a instrução configuration.registerTypeOverride (new UtcTimestampType ()); ?
Stony de
@Stony Onde quer que você inicialize a configuração do Hibernate. Se você tiver um HibernateUtil (a maioria tem), ele estará lá.
Shane de
1
Funciona, mas depois de verificar percebi que não é necessário para o servidor postgres trabalhar em timezone = "UTC" e com todos os tipos de timestamps padrão como "timestamp with time zone" (é feito automaticamente então). Mas, aqui está a versão corrigida do Hibernate 4.3.5 GA como classe única completa e substituindo o bean da fábrica de primavera pastebin.com/tT4ACXn6
Lukasz Frankowski
10

Você pensaria que este problema comum seria resolvido pelo Hibernate. Mas isso não! Existem alguns "hacks" para acertar.

O que eu uso é para armazenar a data como um longo no banco de dados. Portanto, estou sempre trabalhando com milissegundos após 01/01/70. Eu então tenho getters e setters em minha classe que retornam / aceitam apenas datas. Portanto, a API permanece a mesma. O lado ruim é que tenho longos no banco de dados. Assim, com SQL, posso fazer praticamente apenas <,>, = comparações - não operadores de data extravagantes.

Outra abordagem é usar um tipo de mapeamento personalizado conforme descrito aqui: http://www.hibernate.org/100.html

Acho que a maneira correta de lidar com isso é usar um calendário em vez de uma data. Com o calendário, você pode definir o fuso horário antes de persistir.

NOTA: O estúpido stackoverflow não me deixa comentar, então aqui está uma resposta a David a.

Se você criar este objeto em Chicago:

new Date(0);

O Hibernate persiste como "31/12/1969 18:00:00". As datas não devem ter fuso horário, por isso não sei por que o ajuste seria feito.

codificador
fonte
1
Que vergonha! Você estava certo e o link de sua postagem explica bem. Agora, acho que minha resposta merece alguma reputação negativa :)
david a.
1
De modo nenhum. você me encorajou a postar um exemplo muito explícito de porque isso é um problema.
codefinger
4
Consegui persistir os tempos corretamente usando o objeto Calendar para que fossem armazenados no banco de dados como UTC, conforme você sugeriu. Entretanto, ao ler entidades persistentes do banco de dados, o Hibernate assume que elas estão no fuso horário local e o objeto Calendar está incorreto!
John K de
1
John K, para resolver este Calendarproblema de leitura, acho que o Hibernate ou JPA deve fornecer alguma forma de especificar, para cada mapeamento, o fuso horário para o qual o Hibernate deve traduzir a data de leitura e gravação em uma TIMESTAMPcoluna.
Derek Mahar
joekutner, depois de ler stackoverflow.com/questions/4123534/… , eu vim compartilhar sua opinião de que devemos armazenar milissegundos desde a Epoch no banco de dados, em vez de um, uma Timestampvez que não podemos necessariamente confiar no driver JDBC para armazenar datas como que seria de esperar.
Derek Mahar de
8

Existem vários fusos horários em operação aqui:

  1. Classes de data do Java (util e sql), que têm fusos horários UTC implícitos
  2. O fuso horário em que sua JVM está sendo executada e
  3. o fuso horário padrão de seu servidor de banco de dados.

Tudo isso pode ser diferente. O Hibernate / JPA tem uma deficiência severa de design, pois o usuário não pode garantir facilmente que as informações de fuso horário sejam preservadas no servidor de banco de dados (o que permite a reconstrução de horas e datas corretas na JVM).

Sem a capacidade de (facilmente) armazenar o fuso horário usando JPA / Hibernate, as informações são perdidas e, uma vez que as informações são perdidas, torna-se caro construí-las (se possível).

Eu diria que é melhor sempre armazenar informações de fuso horário (deve ser o padrão) e os usuários devem ter a capacidade opcional de otimizar o fuso horário (embora isso realmente afete apenas a exibição, ainda há um fuso horário implícito em qualquer data).

Desculpe, esta postagem não fornece uma solução alternativa (isso foi respondido em outro lugar), mas é uma racionalização de por que sempre armazenar informações de fuso horário é importante. Infelizmente, parece que muitos cientistas da computação e profissionais de programação argumentam contra a necessidade de fusos horários simplesmente porque não apreciam a perspectiva da "perda de informações" e como isso torna coisas como a internacionalização muito difíceis - o que é muito importante hoje em dia com sites acessíveis por clientes e pessoas em sua organização à medida que se movem ao redor do mundo.

Moa
fonte
1
"O Hibernate / JPA tem uma deficiência severa de design" Eu diria que é uma deficiência no SQL, que tradicionalmente permite que o fuso horário seja implícito e, portanto, potencialmente qualquer coisa. SQL bobo.
Raedwald
3
Na verdade, em vez de sempre armazenar o fuso horário, você também pode padronizar em um fuso horário (geralmente UTC) e converter tudo para este fuso horário ao persistir (e de volta ao ler). Isso é o que geralmente fazemos. No entanto, o JDBC também não oferece suporte direto: - /.
sleske
3

Por favor, dê uma olhada em meu projeto no Sourceforge, que tem tipos de usuário para tipos de data e hora SQL padrão, bem como JSR 310 e Joda Time. Todos os tipos tentam resolver o problema da compensação. Veja http://sourceforge.net/projects/usertype/

EDITAR: Em resposta à pergunta de Derek Mahar anexada a este comentário:

"Chris, seus tipos de usuário funcionam com Hibernate 3 ou superior? - Derek Mahar 7 de novembro de 2010 às 12:30"

Sim, esses tipos suportam versões do Hibernate 3.x incluindo o Hibernate 3.6.

Chris Pheby
fonte
2

A data não está em nenhum fuso horário (é um escritório em milissegundo de um momento definido no mesmo tempo para todos), mas os bancos de dados (R) subjacentes geralmente armazenam carimbos de data / hora em formato político (ano, mês, dia, hora, minuto, segundo,. ..) que é sensível ao fuso horário.

Para falar a sério, o Hibernate DEVE permitir que seja informado em alguma forma de mapeamento que a data do banco de dados está em tal e tal fuso horário, de forma que quando ele carrega ou armazena não assume o seu próprio ...


fonte
1

Eu encontrei exatamente o mesmo problema quando eu queria armazenar as datas no banco de dados como UTC e evitar o uso de conversões varcharexplícitas String <-> java.util.Date, ou configurar meu aplicativo Java inteiro no fuso horário UTC (porque isso poderia levar a outros problemas inesperados, se o JVM for compartilhada entre muitos aplicativos).

Portanto, existe um projeto de código aberto DbAssist, que permite corrigir facilmente a leitura / gravação como data UTC do banco de dados. Como você está usando Anotações JPA para mapear os campos na entidade, tudo que você precisa fazer é incluir a seguinte dependência em seu pomarquivo Maven :

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

Em seguida, você aplica a correção (para o exemplo Hibernate + Spring Boot) adicionando uma @EnableAutoConfigurationanotação antes da classe de aplicativo Spring. Para outras instruções de instalação de configurações e mais exemplos de uso, basta consultar o github do projeto .

O bom é que você não precisa modificar as entidades; você pode deixar seus java.util.Datecampos como estão.

5.2.2deve corresponder à versão do Hibernate que você está usando. Não tenho certeza de qual versão você está usando em seu projeto, mas a lista completa de correções fornecidas está disponível na página wiki do github do projeto . A razão pela qual a correção é diferente para várias versões do Hibernate é porque os criadores do Hibernate mudaram a API algumas vezes entre os lançamentos.

Internamente, a correção usa dicas de divestoclimb, Shane e algumas outras fontes para criar um personalizado UtcDateType. Em seguida, ele mapeia o padrão java.util.Datecom o personalizado UtcDateTypeque lida com todo o tratamento de fuso horário necessário. O mapeamento dos tipos é obtido por meio de @Typedefanotações no package-info.javaarquivo fornecido .

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

Você pode encontrar um artigo aqui que explica por que essa mudança de tempo ocorre e quais são as abordagens para resolvê-lo.

orim
fonte
1

O Hibernate não permite especificar fusos horários por anotação ou qualquer outro meio. Se você usar Calendário em vez de data, poderá implementar uma solução alternativa usando a propriedade AccessType de HIbernate e implementando o mapeamento você mesmo. A solução mais avançada é implementar um UserType personalizado para mapear sua data ou calendário. Ambas as soluções são explicadas em minha postagem do blog aqui: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

joobik
fonte