Estou desenvolvendo um aplicativo que armazena alguns dados com registro de data e hora no passado.
Minha JVM é IBM Semeru (17) rodando em Europe/Paris tz. Mas eu quero armazenar o timestamp em UTC (GMT+0).
Abaixo está meu código:
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.1</version>
</dependency>
</dependencies>
public class Main {
/*
create table theentity (
theid integer not null,
thevalue timestamp(6),
primary key (theid)
)
*/
public static void main(String[] args) {
TimeZone.setDefault( TimeZone.getTimeZone( ZoneId.of( "Europe/Paris" ) ) );
LocalDateTime d_1900_01_01_T_00_09_23 = LocalDateTime.of( 1900, 1, 1, 0, 9, 23, 0 );
LocalDateTime d_1900_01_01_T_00_09_22 = LocalDateTime.of( 1900, 1, 1, 0, 9, 22, 0 );
LocalDateTime d_1900_01_01_T_00_09_21 = LocalDateTime.of( 1900, 1, 1, 0, 9, 21, 0 );
LocalDateTime d_1900_01_01_T_00_09_20 = LocalDateTime.of( 1900, 1, 1, 0, 9, 20, 0 );
LocalDateTime d_1900_01_01_T_00_09_19 = LocalDateTime.of( 1900, 1, 1, 0, 9, 19, 0 );
try(Connection c = DriverManager.getConnection( "jdbc:postgresql://localhost:5432/hibernate_orm_test?preparedStatementCacheQueries=0&escapeSyntaxCallMode=callIfNoReturn",
"postgres", "root")) {
PreparedStatement p = c.prepareStatement( "insert into theentity values(?, ?)" );
bindAndExecute( p, 1, d_1900_01_01_T_00_09_23 );
bindAndExecute( p, 2, d_1900_01_01_T_00_09_22 );
bindAndExecute( p, 3, d_1900_01_01_T_00_09_21 );
bindAndExecute( p, 4, d_1900_01_01_T_00_09_20 );
bindAndExecute( p, 5, d_1900_01_01_T_00_09_19 );
} catch (Exception e) {
e.printStackTrace();
}
}
private static void bindAndExecute(PreparedStatement p, int id, LocalDateTime localDateTime)
throws SQLException {
p.setInt( 1, id );
p.setTimestamp(2,
Timestamp.valueOf( localDateTime ),
Calendar.getInstance( TimeZone.getTimeZone( ZoneId.of( "GMT" ) ) )
);
p.executeUpdate();
}
}
Em outras palavras, tentei persistir 5 carimbos de data/hora (observe que esses carimbos de data/hora são baseados na Europa/Paris por causa de TimeZone.setDefault( TimeZone.getTimeZone( ZoneId.of( "Europe/Paris" ) ) );
)
- 1900-01-01T00:09:23
- 1900-01-01T00:09:22
- 1900-01-01T00:09:21
- 1900-01-01T00:09:20
- 1900-01-01T00:09:19
Esta linha traduziria Europa/Paris tz para UTC tz:
p.setTimestamp(2,
Timestamp.valueOf( localDateTime ),
Calendar.getInstance( TimeZone.getTimeZone( ZoneId.of( "GMT" ) ) ) );
Execute-o para criar 5 linhas no postresql:
theid | thevalue
-------+---------------------
1 | 1900-01-01 00:00:02
2 | 1900-01-01 00:00:01
3 | 1900-01-01 00:00:00
4 | 1899-12-31 23:09:20
5 | 1899-12-31 23:09:19
(5 rows)
A maioria de vocês pode pensar que Paris está em GMT+1. Está correto, mas NÃO ESTAVA correto. Às 19h00, estava em GMT+00:09:21 (9 minutos, 21 seg)!!
Então os primeiros 3 timestamps foram salvos corretamente na tabela. Mas, o estranho está na linha 4. Está, 1899-12-31 23:09:20
mas eu esperava que estivesse 1899-12-31 23:59:59
. Mas parece que em 1899, a antiga API DateTime acha que está em GMT+1. Isso fez com que as linhas 4, 5 e assim por diante... estivessem erradas!!
Vocês podem explicar isso?
Você perguntou algo semelhante há uma semana e eu me referi ao problema em um comentário sobre essa questão.
A explicação subjacente - confusão no tzdata e bugs no OpenJDK.
Antes de 1970, os fusos horários do OpenJDK estavam completamente quebrados . Ou seja, nada disso é garantido, e não há nenhuma correção no horizonte: O projeto OpenJDK não está disposto a entender como as políticas do tzdata foram alteradas ou não está disposto a dar suporte a horários adequados antes de 1970 se eles entenderem o que a atualização de 2022b para o projeto tzdata significa.
O que é tzdata?
tzdata é um projeto separado usado por muitas coisas, incluindo kernels Linux e JDKs; o projeto é um arquivo com definições exatas de muitos fusos horários e dados históricos sobre como e quando eles mudaram ao longo do tempo.
Dado o quão difícil é historicamente determinar precisamente qual era o fuso horário de um país em qualquer momento no passado distante, o conjunto base do tzdata não garante mais nada se for sobre informações de fuso horário anteriores a 1970. Por, eu acho, razões históricas, ele ainda envia um monte de definições e mudanças anteriores a 1970, mas não garante que elas estejam corretas. E de fato elas frequentemente não estão: coisas simplificadas demais, muitas vezes conhecidas como incorretas. Alguns tentam fazer malabarismos com simplicidade e redução do tamanho das tabelas tz com serem pelo menos um pouco precisos no trabalho, eu presumo.
Isso não é culpa do tzdata; o princípio central dessa mudança em como o tzdata funciona está, em essência, correto: não é possível ter muita certeza sobre dados de fuso horário anteriores a 1970; há muito poucos registros que podem ser facilmente verificados com o orçamento extremamente limitado de código aberto (tzdata é um projeto de código aberto executado mais ou menos por uma pessoa; eles não podem voar para Paris e verificar algumas coisas de microfilme na biblioteca nacional ou algo assim), e as opiniões variadas sobre quais lugares sequer existiram ficam muito confusas. Deveríamos ter um
Europe/Vichy
? E quanto aPrussia/Koningsberg
(parte da Alemanha unificada antes da primeira guerra mundial, mudou de mãos um pouco e agora é a moderna Kaliningrado na Rússia?)Mas, antes de 2022 ou mais, o tzdata tentou fazer tudo e ser o mais preciso possível. Mas, desde então, não mais: em vez disso, há um arquivo tzdata estendido que é notavelmente menos "verificado" (ou seja, se você tentar registrar um bug contra os dados estendidos sobre alguns meses obscuros onde, sem dúvida, com base em evidências difíceis de verificar, um fuso horário diferente foi usado por um tempo, ele pode ser ignorado, mesmo que, no geral, seja mais provável que seja verdade do que não, e certamente não será pego com alta prioridade).
Mas esse arquivo estendido corresponde perfeitamente à aparência do tzdata em 2022.
O OpenJDK tem apenas o arquivo tzdata base (verificado somente após 1970); ele não inclui as definições estendidas .
Portanto, o bug: Europa/Paris pré-1970 provavelmente está incorreto. Eu sei com certeza que Europa/Amsterdã no intervalo de 1938-1945 está incorreto.
Como é isso?
Com um JDK baseado na versão tzdata 2022a ou anterior, este código:
Isso imprime -1022980800000L em JDKs 'corretos' (neste ponto, bem antigos, construídos antes de 2022). Isso é, até onde todos os fãs de história concordam, o tempo correto.
Mas em JDKs modernos, isso não é impresso; não mais. Desde que os JDKs integraram o tzdata 2022-b (o primeiro lançamento com coisas simplificadas pré-1970), você obtém um número diferente e errado.
O efeito disso no seu software depende de como você usa as datas ou como seus departamentos as usam.
Como um exemplo específico, H2 (o mecanismo de banco de dados) vai bagunçar completamente . Se você armazenar uma data, mesmo que faça isso com a estratégia altamente recomendada de escrever com
preparedStatement.setObject(LocalDate.of(1937, 8, 2))
e ler com o recomendadoresultSet.getObject(colIdx, LocalDate.class)
(que a especificação JDBC garante que funcionará em qualquer implementação JDBC compatível), e a data que você escreveu em um JDK pré-2022b e leu de volta em um JDK pós-2022b responderá com uma data que é única.E 1937 é um ano totalmente plausível para nascer. Datas de nascimento são usadas como chaves em muitas circunstâncias (especialmente médicas), então isso quebra tudo.
Consertando - passo 1
Você precisa fazer duas coisas para consertar seu JDK. O passo 1 é consertar os arquivos tzdata:
Há a ferramenta tzupdater da Oracle que permite que você reescreva os arquivos tzdata usados por um JDK. Baixe-a e use-a da seguinte forma em um sistema posix:
Note que isso significa que qualquer atualização de tz desde 2022 (ei, entidades políticas às vezes decretam mudanças de fuso horário, isso acontece) são removidas ao mudar para esta definição; 2022a é a última vez que o conjunto foi 'pré-1970 melhor esforço'. Não tenho uma solução se você também precisar de alguma mudança pós-2022a neste momento.
Isso atualizará a instalação do JDK que está fornecendo o executável Java com o qual você executa esse comando. Naturalmente, você precisa de acesso de administrador, pois o JDK precisará reescrever seus próprios arquivos para fazer isso.
Etapa 2 - Tenha muito cuidado com as transições de data para milissegundos
Um design de BD adequado armazenará um LocalDate como seus campos componentes: Uma data de 1937-08-02 deve ser armazenada exatamente assim : Armazene esses 3 números. Use um pouco de ajuste de bits para colocá-los em tão poucos bits quanto você puder reunir, se quiser, mas, infelizmente, não é isso que o H2 v1 faz; em vez disso, ele usa algum fuso horário decretado para convertê-lo para epoch-millis naquela data à meia-noite, armazena isso e, ao ler esses dados, o mesmo aspecto é feito ao contrário. É por isso que o H2 retornou a data errada, mesmo se você usar
LocalDate
'in' e 'out'.O psql, por exemplo, não teria feito isso e retorna uma data estável mesmo se você 'escrever' os dados em um JDK com 2022a e 'lê-los' em 2022b ou superior.
No entanto, muito importante:
java.sql.Timestamp
and especialmentejava.sql.Date
estão totalmente quebrados e você não deve usá-los . Infelizmente, muitas abstrações, como JPA impls, costumam fazer isso, não sei como consertar isso. O problema é que esses 2 tipos extendsjava.util.Date
e essa classe está quebrada (é por isso que está obsoleta): É mentira. Ela não representa datas, representa instantes. É por isso que todos os métodos relacionados a datas (comodate.getYear()
) estão obsoletos: Porque a resposta que eles dão pode estar errada e isso não pode ser corrigido. E porquejava.sql.Date
andjava.sql.Timestamp
estende essa classe quebrada, eles também estão quebrados.A ÚNICA maneira de usar datas corretamente em bancos de dados é rezar para que seu BD as armazene corretamente (o que infelizmente o H2 v1 não faz) e que você use a maneira correta de retransmitir dados de data/hora para dentro e para fora do BD:
escrever:
onde você pode passar qualquer instância do tipo
LocalDate
,LocalDateTime
,Instant
, ouOffsetDateTime
. Infelizmente, o tipo de data/hora que é mais frequentemente a coisa mais apropriada éZonedDateTime
e isso não é suportado por nenhum DB que eu conheço, ou não é suportado corretamente (eles não sabem como fazer um pouco de bittwiddle do bastante prolixoEurope/Paris
em alguns bits, eu acho, faz algum sentido que eles achem isso difícil de armazenar).Para ler:
Onde você também pode usar
Instant
orLocalDateTime
, orOffsetDateTime
em vez deLocalDate
.Por que não existe um
.getLocalDate
likegetDate
?Porque o painel JDBC decidiu parar de adicionar um
getType()
método para cada novo tipo que surge como DB base <-> tipo Java; nenhum método desse tipo será criado novamente. égetObject
isso. Em vez disso, a especificação JDBC começou a decretar certos tipos como ' devem ser suportados'. LocalDate e os outros tipos nomeados acima estão na lista. ZonedDateTime não está.Naturalmente, você mesmo também nunca deve converter 'cálculo humano' (tempo declarado em termos de anos, meses, horas, semanas, esse tipo de coisa) para 'cálculo de computador' (época millis). Ou, pelo menos, nunca converter apenas para converter de volta mais tarde, porque essa operação não é reversível, embora você pense que seria. "Converta este valor de hora:minuto:segundo de ano/mês/dia em Paris para epochmillis... e então amanhã eu vou pedir para você converter isso de volta" não é realmente garantido para lhe dar o mesmo valor. Então, não faça isso. Armazene suas coisas de data/hora exatamente como estão. Não pense que
Instant
+TimeZone
é perfeitamente conversível para frente e para trás para umaZonedDateTime
instância. Porque tzdata pode mudar.Cuidado com as atualizações automáticas!
Se a plataforma do seu servidor atualizar a versão do JDK, a atualização do tzdata será substituída.
Sugiro que você execute este código na inicialização do seu aplicativo Java e saia imediatamente se detectar que o tzdata não está atualizado; executá-lo neste ponto fará com que seu aplicativo comece a corromper seu banco de dados, e você não quer isso: