AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / coding / Perguntas / 79437089
Accepted
SoT
SoT
Asked: 2025-02-14 00:40:13 +0800 CST2025-02-14 00:40:13 +0800 CST 2025-02-14 00:40:13 +0800 CST

Jdbc persiste com timestamp errado em 1900

  • 772

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:20mas 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?

java
  • 1 1 respostas
  • 75 Views

1 respostas

  • Voted
  1. Best Answer
    rzwitserloot
    2025-02-14T07:57:10+08:002025-02-14T07:57:10+08:00

    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 a Prussia/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:

    // Warning: Ordinarily, don't use the `Calendar` API,
    // it's bad. But to reproduce this bug it's slightly easier.
    // Using `java.time` (or `java.util.Date`) does not make it go away.
    
            var gc = new GregorianCalendar(TimeZone.getTimeZone("Europe/Amsterdam"));
            gc.set(Calendar.YEAR, 1937);
            gc.set(Calendar.MONTH, Calendar.AUGUST);
            gc.set(Calendar.DAY_OF_MONTH, 2);
            gc.set(Calendar.HOUR_OF_DAY, 0);
            gc.set(Calendar.MINUTE, 0);
            gc.set(Calendar.SECOND, 0);
            gc.set(Calendar.MILLISECOND,0);
            System.out.println(gc.getTimeInMillis());
    

    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 recomendado resultSet.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:

    sudo java -jar tzupdater.jar -v -f -l https://www.iana.org/time-zones/repository/releases/tzdata2022a.tar.gz
    

    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.Timestampand especialmente java.sql.Dateestã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 extends java.util.Datee 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 (como date.getYear()) estão obsoletos: Porque a resposta que eles dão pode estar errada e isso não pode ser corrigido. E porque java.sql.Dateand java.sql.Timestampestende 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:

    preparedStatement.setObject(qIdx, instanceOfProperType);
    

    onde você pode passar qualquer instância do tipo LocalDate, LocalDateTime, Instant, ou OffsetDateTime. Infelizmente, o tipo de data/hora que é mais frequentemente a coisa mais apropriada é ZonedDateTimee 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 prolixo Europe/Parisem alguns bits, eu acho, faz algum sentido que eles achem isso difícil de armazenar).

    Para ler:

    LocalDate x = resultset.getObject(colIdxOrName, LocalDate.class);
    

    Onde você também pode usar Instantor LocalDateTime, or OffsetDateTimeem vez de LocalDate.

    Por que não existe um .getLocalDatelike getDate?

    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 uma ZonedDateTimeinstâ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:

        private void checkTimezoneBackzoneIssue() {
            var gc = new GregorianCalendar(TimeZone.getTimeZone("Europe/Amsterdam"));
            gc.set(Calendar.YEAR, 1937);
            gc.set(Calendar.MONTH, Calendar.AUGUST);
            gc.set(Calendar.DAY_OF_MONTH, 2);
            gc.set(Calendar.HOUR_OF_DAY, 0);
            gc.set(Calendar.MINUTE, 0);
            gc.set(Calendar.SECOND, 0);
            gc.set(Calendar.MILLISECOND,0);
            if (gc.getTimeInMillis() != -1022980800000L) {
                System.err.println("Timezone data is broken: It no longer contains pre-1970 info (the 'backzone'), which breaks Europe/Amsterdam particularly badly; all ");
                System.err.println("dates prior to May 20th 1940 will be read back wrong, this affects birthdays stored in H2 databases, for example.");
                System.err.println("The JVM will now be shut down to avoid data corruption.");
                System.err.println("---");
                System.err.println("You can 'fix' a JDK using https://www.oracle.com/java/technologies/downloads/tools/#TZUpdater, running java -jar tzupdater.jar -v -f -l https://www.iana.org/time-zones/repository/releases/tzdata2022a.tar.gz");
                System.err.println("Your java is at: " + System.getProperty("java.home"));
                System.exit(18);
            }
        }
    
    • 4

relate perguntas

  • Lock Condition.notify está lançando java.lang.IllegalMonitorStateException

  • Resposta de microsserviço Muitos para Um não aparece no carteiro

  • Validação personalizada do SpringBoot Bean

  • Os soquetes Java são FIFO?

  • Por que não é possível / desencorajado definir um lado do servidor de tempo limite de solicitação?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Reformatar números, inserindo separadores em posições fixas

    • 6 respostas
  • Marko Smith

    Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não?

    • 2 respostas
  • Marko Smith

    Problema com extensão desinstalada automaticamente do VScode (tema Material)

    • 2 respostas
  • Marko Smith

    Vue 3: Erro na criação "Identificador esperado, mas encontrado 'import'" [duplicado]

    • 1 respostas
  • Marko Smith

    Qual é o propósito de `enum class` com um tipo subjacente especificado, mas sem enumeradores?

    • 1 respostas
  • Marko Smith

    Como faço para corrigir um erro MODULE_NOT_FOUND para um módulo que não importei manualmente?

    • 6 respostas
  • Marko Smith

    `(expression, lvalue) = rvalue` é uma atribuição válida em C ou C++? Por que alguns compiladores aceitam/rejeitam isso?

    • 3 respostas
  • Marko Smith

    Um programa vazio que não faz nada em C++ precisa de um heap de 204 KB, mas não em C

    • 1 respostas
  • Marko Smith

    PowerBI atualmente quebrado com BigQuery: problema de driver Simba com atualização do Windows

    • 2 respostas
  • Marko Smith

    AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos

    • 1 respostas
  • Martin Hope
    Fantastic Mr Fox Somente o tipo copiável não é aceito na implementação std::vector do MSVC 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant Encontre o próximo dia da semana usando o cronógrafo 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor O inicializador de membro do construtor pode incluir a inicialização de outro membro? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul O C++20 mudou para permitir a conversão de `type(&)[N]` de matriz de limites conhecidos para `type(&)[]` de matriz de limites desconhecidos? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann Como/por que {2,3,10} e {x,3,10} com x=2 são ordenados de forma diferente? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller O ponto e vírgula agora é opcional em condicionais bash com [[ .. ]] na versão 5.2? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench Por que um traço duplo (--) faz com que esta cláusula MariaDB seja avaliada como verdadeira? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng Por que `dict(id=1, **{'id': 2})` às vezes gera `KeyError: 'id'` em vez de um TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos 2024-03-20 03:12:31 +0800 CST

Hot tag

python javascript c++ c# java typescript sql reactjs html

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve