Armazenamento de compromissos em um banco de dados SQL, como Postgres, para uso com o framework java.time

Oct 27 2020

Digamos que temos um compromisso em Milão Itália que acontece no dia 23/01/2021 21:00 "Europa / Roma". Esse compromisso é salvo no banco de dados em UTC em uma coluna de um tipo semelhante ao tipo padrão SQL TIMESTAMP WITH TIME ZONE.

Agora, um usuário que mora em Nova York, nos Estados Unidos, precisa saber quando esse compromisso será realizado. Podemos mostrar ao usuário a data-hora convertida para o fuso horário "America / New_York" ou, em vez disso, em "Europe / Rome" TZ. Assim que o usuário voar de Nova York a Milão, ele achará as duas informações úteis.

O objetivo é armazenar tudo convertido na mesma referência TZ (UTC) e manipular a data-hora dependendo do objetivo que você tem usando a estrutura java.time empacotada com o Java moderno.

Faz sentido ou há algo errado / faltando?

Respostas

4 BasilBourque Oct 27 2020 at 04:57

Faz sentido ou há algo errado / faltando?

Depende do tipo de consulta.

Existem dois tipos de compromissos:

  • Um momento, um ponto específico na linha do tempo, ignorando qualquer mudança nas regras de fuso horário.
    Exemplo: lançamento de foguete.
  • Uma data e hora do dia que deve se ajustar às alterações nas regras de fuso horário.
    Exemplo: Consulta Médica / Odontológica.

Momento

Se estamos reservando o lançamento de um foguete, por exemplo, não nos importamos com a data e a hora do dia. Só nos preocupamos com o momento em que (a) os céus se alinham e (b) esperamos um clima favorável.

Se, nesse período, os políticos mudarem as regras do fuso horário em uso em nosso local de lançamento ou em nossos escritórios, isso não afetará nosso compromisso de lançamento. Se os políticos que governam nosso local de lançamento adotarem o horário de verão (DST) , o momento de nosso lançamento permanecerá o mesmo. Se os políticos que governam nossos escritórios decidirem mudar o relógio meia hora antes por causa das relações diplomáticas com um país vizinho, o momento do nosso lançamento permanece o mesmo.

Para tal nomeação, sim, sua abordagem seria correta. Você registraria o compromisso em UTC usando uma coluna do tipo TIMESTAMP WITH TIME ZONE. Após a recuperação, ajuste em qualquer fuso horário que o usuário preferir.

Bancos de dados como o Postgres usam qualquer informação de fuso horário que acompanha uma entrada para se ajustar ao UTC e, em seguida, descarta essa informação de fuso horário. Quando você recupera o valor do Postgres, ele sempre representará uma data com a hora do dia, conforme visto em UTC. Cuidado, algumas ferramentas ou middleware podem ter o anti-recurso de aplicar algum fuso horário padrão entre a recuperação do banco de dados e a entrega a você, o programador. Mas seja claro: o Postgres sempre salva e recupera valores do tipo TIMESTAMP WITH TIME ZONEem UTC, sempre UTC e deslocamento do UTC de zero horas-minutos-segundos.

Aqui está um exemplo de código Java.

LocalDate launchDateAsSeenInRome = LocalDate.of( 2021 , 1 , 23 ) ;
LocalTime launchTimeAsSeenInRome = LocalTime.of( 21 , 0 ) ;
ZoneId zoneEuropeRome = ZoneId.of( "Europe/Rome" ) ;
// Assemble those three parts to determine a moment.
ZonedDateTime launchMomentAsSeenInRome = ZonedDateTime.of( launchDateAsSeenInRome , launchTimeAsSeenInRome , zoneEuropeRome ) ;

launchMomentAsSeenInRome.toString (): 2021-01-23T21: 00 + 01: 00 [Europa / Roma]

Para ver o mesmo momento em UTC, converta para um Instant. Um Instantobjeto sempre representa um momento visto em UTC.

Instant launchInstant = launchMomentAsSeenInRome.toInstant() ;  // Adjust from Rome time zone to UTC.

launchInstant.toString (): 2021-01-23T20: 00: 00Z

O Zno final do exemplo de string acima é a notação padrão para UTC e é pronunciado “Zulu”.

Infelizmente, a equipe JDBC 4.2 não solicitou suporte para os tipos Instantou ZonedDateTime. Portanto, seu driver JDBC pode ou não ser capaz de ler / gravar tais objetos em seu banco de dados. Caso contrário, basta converter para OffsetDateTime. Todos os três tipos representam um momento, um ponto específico na linha do tempo. Mas OffsetDateTimetem suporte exigido pelo JDBC 4.2 por motivos que me escapam.

OffsetDateTime odtLaunchAsSeenInRome = launchMomentAsSeenInRome.toOffsetDateTime() ;

Gravando no banco de dados.

myPreparedStatement.setObject( … , odtLaunchAsSeenInRome ) ;

Recuperação do banco de dados.

OffsetDateTime launchMoment = myResultSet.getObject( … , OffsetDateTime.class ) ;

Ajuste para o fuso horário de Nova York desejado por seu usuário.

ZoneId zoneAmericaNewYork = ZoneId.of( "America/New_York" ) ;
ZonedDateTime launchAsSeenInNewYork = launchMoment.atZoneSameInstant( zoneAmericaNewYork ) ;

launchAsSeenInNewYork.toString (): 2021-01-23T15: 00-05: 00 [America / New_York]

Você pode ver todo o código acima executado ao vivo em IdeOne.com .

A propósito, o rastreamento de eventos passados ​​também é tratado como um momento. Quando o paciente realmente chegou para a consulta, quando o cliente pagou a fatura, quando um novo contratado assinou seus documentos, quando o servidor travou ... tudo isso é rastreado como um momento no UTC. Como discutido acima, geralmente isso seria um Instant, embora ZonedDateTimee OffsetDateTimetambém representasse um momento. Para o uso do banco de dados TIMESTAMP WITH TIME ZONE(não WITHOUT).

Hora do dia

Espero que a maioria dos aplicativos voltados para negócios seja focada em compromissos de outro tipo, onde visamos uma data com uma hora do dia em vez de um momento específico.

Se o usuário marcar uma consulta com seu médico para revisar os resultados de um teste, ele o fará em um determinado horário do dia naquela data. Se, entretanto, os políticos alteram as regras do seu fuso horário, adiantando ou atrasando o relógio uma hora ou meia hora ou qualquer outro período, a data e a hora dessa consulta médica permanecem as mesmas. Na realidade, o ponto da linha do tempo da nomeação original será alterado depois que os políticos mudarem o fuso horário, mudando para um ponto anterior / posterior na linha do tempo.

Para esses compromissos, não armazenamos a data e a hora do dia conforme UTC. Nós não utilizar o tipo de coluna de banco de dados TIMESTAMP WITH TIME ZONE.

Para tais compromissos, armazenamos a data com a hora do dia sem qualquer consideração ao fuso horário. Usamos uma coluna de banco de dados do tipo TIMESTAMP WITHOUT TIME ZONE(aviso em WITHOUTvez de WITH). O tipo de correspondência em Java é LocalDateTime.

LocalDate medicalApptDate = LocalDate.of( 2021 , 1 , 23 ) ;
LocalTime medicalApptTime = LocalTime.of( 21 , 0 ) ;
LocalDateTime medicalApptDateTime = LocalDateTime.of( medicalApptDate , medicalApptTime ) ;

Grave isso no banco de dados.

myPreparedStatement.setObject( … , medicalApptDateTime ) ;

Seja claro: um LocalDateTimeobjeto não representa um momento, não é um ponto específico na linha do tempo. Um LocalDateTimeobjeto representa um intervalo de momentos possíveis ao longo de cerca de 26-27 horas da linha do tempo (o intervalo de fusos horários ao redor do globo). Para dar um significado real a a LocalDateTime, devemos associar um fuso horário pretendido.

Para o fuso horário pretendido, use uma segunda coluna para armazenar o identificador de zona. Por exemplo, as strings Europe/Romeou America/New_York. Veja a lista de nomes de zonas .

ZoneId zoneEuropeRome = ZoneId.of( "Europe/Rome" ) ;

Escreva isso no banco de dados como texto.

myPreparedStatement.setString( … , zoneEuropeRome ) ;

Recuperação. Recupere o nome da zona como texto e instancie um ZoneIdobjeto.

LocalDateTime medicalApptDateTime = myResultSet.getObject( … , LocalDateTime.class ) ;
ZoneId medicalApptZone = ZoneId.of( myResultSet.getString( … ) ) ;

Junte essas duas peças para determinar um momento representado como um ZonedDateTimeobjeto. Faça isso dinamicamente quando precisar agendar um calendário. Mas não guarde o momento. Se os políticos redefinirem o (s) fuso (s) horário (s) no futuro, um momento diferente deve ser calculado então.

ZonedDateTime medicalApptAsSeenInCareProviderZone = ZonedDateTime.of( medicalApptDateTime , medicalApptZone ) ;

O usuário está viajando para Nova York, EUA. Eles precisam saber quando ligar para o provedor de serviços de saúde em Milão, Itália, de acordo com os relógios na parede em sua localização temporária em Nova York. Portanto, ajuste de um fuso horário para outro. Mesmo momento, hora diferente do relógio de parede.

ZoneId zoneAmericaNewYork = ZoneId.of( "America/New_York" ) ;
ZonedDateTime medicalApptAsSeenInNewYork = medicalApptAsSeenInCareProviderZone.withZoneSameInstant( zoneAmericaNewYork ) ;

tzdata

Esteja ciente de que se as regras de seus fusos horários desejados estiverem mudando, você deve atualizar a cópia das definições de fuso horário em seus computadores.

Java contém sua própria cópia do tzdata , assim como o mecanismo de banco de dados Postgres. E seu sistema operacional host também. Este código específico mostrado aqui requer apenas o Java para ser atualizado. Se você usar o Postgres para fazer ajustes de fuso horário, seu tzdata também deve estar atualizado. E para registro e tal, seu sistema operacional host deve ser mantido atualizado. Para uma observação adequada do relógio pelo usuário, o sistema operacional de sua máquina cliente também deve estar atualizado.

Cuidado: os políticos de todo o mundo mostraram uma tendência para alterar seus fusos horários com uma frequência surpreendente e, muitas vezes, com poucos avisos.


Sobre java.time

A estrutura java.time é integrada ao Java 8 e posterior. Essas classes suplantar os velhos problemáticos legados aulas de data e hora, como java.util.Date, Calendar, e SimpleDateFormat.

Para saber mais, consulte o tutorial Oracle . E pesquise no Stack Overflow para muitos exemplos e explicações. A especificação é JSR 310 .

O projeto Joda-Time , agora em modo de manutenção , aconselha a migração para as classes java.time .

Você pode trocar objetos java.time diretamente com seu banco de dados. Use um driver JDBC compatível com JDBC 4.2 ou posterior. Não há necessidade de cordas, não há necessidade de java.sql.*classes. Hibernate 5 e JPA 2.2 suportam java.time .

Onde obter as classes java.time?

  • Java SE 8 , Java SE 9 , Java SE 10 , Java SE 11 e posterior - Parte da API Java padrão com uma implementação agrupada.
    • Java 9 trouxe alguns pequenos recursos e correções.
  • Java SE 6 e Java SE 7
    • A maior parte da funcionalidade java.time é portada para Java 6 e 7 no ThreeTen-Backport .
  • Android
    • Versões posteriores do Android (26+) agrupam implementações das classes java.time .
    • Para o Android anterior (<26), um processo conhecido como API desugaring traz um subconjunto da funcionalidade java.time não originalmente integrada ao Android.
      • Se o Dessacarificação não oferece o que você precisa, o ThreeTenABP projeto adapta ThreeTen-Backport (mencionados acima) para Android. Consulte Como usar o ThreeTenABP… .
LaurenzAlbe Oct 27 2020 at 14:09

Uma solução muito simples seria deixar a conversão para PostgreSQL. Se você definir o timezoneparâmetro corretamente para cada sessão e usar o timestamp with time zonePostgreSQL, mostrará automaticamente a data e hora para o usuário de Nova York no horário de Nova York.