Sei que isso pode parecer estranho após 3 anos de desenvolvimento de software, mas estou tentando entender melhor as classes java.time e como usá-las para evitar erros e práticas inadequadas.
No primeiro projeto em que estive envolvido, a versão java utilizada foi a 8, mas os valores de data foram armazenados na classe legada java.util.Date
Dois anos depois, nova empresa, novo projeto com java versão 17, mas ainda valores de data são armazenados na classe legada java.util.Date
A confusão começou quando vi que havia um DateUtils no projeto com métodos como:
public static String toString_ddMMyyyy_dotted(Date date){
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefualt()).toLocalDateTime();
DateTimeFormatter.ofPattern("dd.MM.yyyy").format(localDateTime);
}
public static Date getBeginOfTheYear(Date date){
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefualt()).toLocalDate();
LocalDate beginOfTheYear = localDate.withMonth(1).withDayOfMonth(1);
return Date.from(beginOfTheYear.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
}
Então minha pergunta foi: por que não usar diretamente as classes java.time que possuem APIs realmente boas em vez de converter uma variável em LocalDate/LocalDateTime sempre que uma operação na Data for necessária?
A ideia é refatorar o projeto o mais rápido possível para armazenar valores de Data/DataHora/Hora em classes java.time. Comecei com um módulo simples, que salva alguns dados no MongoDB. Apliquei a seguinte conversão de tipo de campo:
- Valores de data em LocalDate
- Valores de data e hora em LocalDateTime
Durante o teste, encontrei imediatamente um problema, uma variável localDateTime com valor de 2023-10-07 12:00:00 foi salva no MongoDB como 2023-10-07 10:00:00 Isso não é um problema, desde que os dados é buscado de volta em java, o valor volta para 2023-10-07 12:00:00, mas isso não está acontecendo, então é um grande problema.
Primeiramente, isso acontece apenas com o LocalDateTime ou também acontece com o LocalDate?
Quais são as melhores práticas para armazenar valores de data/data e hora em classes de entidade/documento? Devo usar ZonedDateTime em vez de LocalDateTime?
Conversão em vez de renovação
Dependendo do tamanho e da complexidade da sua base de código, isso pode ser bastante arriscado.
Uma abordagem mais conservadora é fazer toda a nova programação em java.time . Para interoperar com código antigo ainda não atualizado para java.time , converta. Você encontrará novos métodos de conversão nas classes antigas. Procure
to…
efrom…
métodos. Esses métodos de conversão fornecem cobertura completa, permitindo que você vá e volte.Outra palavra de cautela ao renovar o código: muitos/a maioria dos programadores não entendem bem o tratamento de data e hora. O tópico é surpreendentemente complicado e complicado, os conceitos escorregadios quando encontrados pela primeira vez. Nossa compreensão cotidiana de data e hora realmente funciona contra nós quando lidamos com o rigor da programação e dos bancos de dados. Portanto, tome cuidado com códigos ruins, códigos com bugs, que manipulam mal a data e a hora. Cada vez que você descobrir esse código defeituoso, você estará abrindo uma série de problemas, pois relatórios podem ter sido produzidos mostrando resultados incorretos, bancos de dados podem armazenar dados inválidos, etc.
Você pode pesquisar perguntas e respostas existentes do Stack Overflow para saber mais. Abaixo estão alguns breves pontos para ajudar a orientá-lo.
Momento versus Não-momento
Incorreta. Ou incompleto, devo dizer.
Eu recomendo que você estruture seu pensamento desta forma… Existem dois tipos de controle de tempo:
Momento
Um momento é um ponto específico na linha do tempo. Os momentos são rastreados em java.time como uma contagem de segundos inteiros, mais um segundo fracionário como uma contagem de nanossegundos, desde a referência de época do primeiro momento de 1970 em UTC. "in UTC" é a abreviação de "um deslocamento do meridiano temporal do UTC de zero horas-minutos-segundos".
A classe básica para representar um momento é
java.time.Instant
. Objetos desta classe representam um momento visto em UTC, sempre em UTC. Esta classe é o bloco de construção básico da estrutura jav.time . Esta aula deve ser seu primeiro e último pensamento ao lidar com momentos. UseInstant
para rastrear um momento, a menos que suas regras de negócios envolvam especificamente uma zona ou deslocamento específico. Ejava.util.Date
é especificamente substituído porjava.time.Instant
, ambos representando um momento no UTC, mas comInstant
uma resolução mais precisa de nanossegundos em relação àjava.util.Date
resolução de milissegundos.Duas outras classes rastreiam momentos em java.time :
OffsetDateTime
&ZonedDateTime
.OffsetDateTime
representa um momento visto com um deslocamento do UTC diferente de zero. Um deslocamento é apenas um número de horas-minutos-segundos à frente ou atrás do UTC. Assim, as pessoas em Paris acertam os relógios nas paredes uma ou duas horas antes do UTC, enquanto as pessoas em Tóquio acertam os relógios nove horas antes, enquanto as pessoas na Nova Escócia, Canadá, acertam os relógios três ou quatro horas antes do UTC .Um fuso horário é muito mais do que um mero deslocamento. Um fuso horário é uma história nomeada de mudanças passadas, presentes e futuras no deslocamento usado pelas pessoas de uma determinada região, conforme decidido por seus políticos. Um fuso horário tem um nome no formato
Continent/Region
comoEurope/Paris
&Asia/Tokyo
.Para visualizar a data e a hora do dia através do horário de uma determinada região, use
ZonedDateTime
.Observe que você pode alternar de maneira fácil e limpa entre essas três classes,
Instant
,OffsetDateTime
eZonedDateTime
por meio de seus métodosto…
,at…
e .with…
Eles fornecem três maneiras de olhar para o mesmo momento. Geralmente você usaInstant
em sua lógica de negócios e armazenamento e troca de dados, enquanto usaZonedDateTime
para apresentação ao usuário.Nem um momento
Por outro lado, não temos rastreamento “nem um momento”. Esses são os tipos indefinidos.
A classe que não causa mais confusão é
LocalDateTime
. Esta classe representa uma data com hora do dia, mas sem qualquer conceito de deslocamento ou fuso horário. Portanto, se eu disser meio-dia no próximo dia 23 de janeiro de 2024,LocalDateTime.of( 2024 , Month.JANUARY , 23 , 12 , 0 , 0 , 0 )
você não tem como saber se me refiro ao meio-dia em Tóquio, ao meio-dia em Toulouse ou ao meio-dia em Toledo, Ohio, EUA - três momentos muito diferentes com várias horas de intervalo.Na dúvida, não use
LocalDateTime
. Geralmente, na programação de aplicativos de negócios, nos preocupamos com os momentos e, portanto, raramente usamos arquivosLocalDateTime
.A grande exceção, quando mais precisamos
LocalDateTime
em aplicativos empresariais, é o agendamento de consultas, como para o dentista. Lá, precisamos manter aLocalDateTime
e fuso horário (ZoneId
) separadamente , em vez de combiná-los em aZonedDateTime
. A razão é crucial: os políticos mudam frequentemente as regras do fuso horário, e fazem isso de forma imprevisível e muitas vezes com pouco aviso prévio. Portanto, a consulta do dentista às 15h pode ocorrer uma hora antes, ou uma hora depois, se os políticos decidirem adotar o horário de verão (DST), ou decidirem abandonar o horário de verão, ou decidirem permanecer no horário de verão o ano todo (o último moda), ou decidir que uma compensação diferente pode trazer oportunidades de negócios (ex: Islândia adotando uma compensação de zero) ou resolver questões políticas (ex: um único fuso horário em toda a Índia), ou decidir ajustar seu relógio como um movimento diplomático em relação a um país vizinho, ou deve adotar a compensação de uma força invasora/ocupante. Tudo isso acontece com muito mais frequência do que a maioria das pessoas, inclusive os programadores, imaginam. Essas mudanças quebram desnecessariamente aplicativos escritos de forma ingênua.Os outros tipos indefinidos incluem:
LocalDate
para um valor somente de data, sem hora do dia e sem qualquer zona/deslocamento.LocalTime
para um valor somente de tempo sem qualquer data e sem qualquer zona/deslocamento.Isso
LocalDate
pode ser complicado porque muitas pessoas não sabem que, em um determinado momento, a data varia em todo o mundo de acordo com o fuso horário. Agora é “amanhã” em Tóquio, Japão, e simultaneamente “ontem” em Edmonton, Canadá.Seu código
Esse código tem dois problemas.
LocalDateTime
. Ajava.util.Date
classe atrásdate
de var representa um momento com deslocamento zero. Você descarta a informação crucial, o deslocamento, ao converter paraLocalDateTime
.ZoneId.systemDefualt()
. Isso significa que os resultados variam dependendo do fuso horário padrão atual da JVM. Conforme discutido acima, isso significa que a data pode variar, não apenas a hora do dia. O mesmo código executado em duas máquinas diferentes pode produzir duas datas diferentes. Isso pode ser o que você deseja em seu aplicativo ou pode ser um rude despertar para um programador inconsciente.Seu outro trecho de código também apresenta problemas.
Em primeiro lugar, esteja ciente de que, infelizmente, existem duas
Date
classes entre as classes herdadas de data e hora:java.util.Date
por um momento, ejava.sql.Date
que finge representar apenas uma data, mas na verdade é um momento (em uma decisão terrivelmente ruim de design de classe). Eu assumirei seus meiosjava.util.Date
.Em segundo lugar, evite esses tipos de cadeias de chamadas com esse tipo de código. A legibilidade, a depuração e o registro ficam mais difíceis. Em vez disso, escreva linhas curtas e simples ao fazer uma série de conversões. Use comentários em cada linha para justificar a operação e explicar sua motivação. E evite o uso
var
pelo mesmo motivo; use tipos de retorno explícitos ao fazer tais conversões.Isso
date.toInstant()
é bom. Ao encontrar umjava.util.Date
, converta imediatamente para umInstant
.Novamente, o uso de
ZoneId.systemDefualt()
significa que os resultados do seu código variam de acordo com o capricho de qualquer administrador de sistema ou usuário que esteja alterando o fuso horário padrão. Esta pode ser a intenção do autor deste código, mas duvido.A parte
beginOfTheYear.atStartOfDay()
produz umLocalDateTime
quando você não passa nenhum argumento. Então você está novamente descartando informações valiosas (compensação) sem ganhar nada em troca.Outro problema: seu código nem será compilado. O
java.util.Date.from
método recebe umInstant
, não umLocalDateTime
retornado pela sua chamadabeginOfTheYear.atStartOfDay()
.Para obter corretamente o primeiro momento do ano de um determinado momento, é quase certo que você desejaria que as pessoas que decidem as regras de negócios ditassem um fuso horário específico.
Você realmente deve converter a entrada
java.util.Date
em um arquivoInstant
.Em seguida, aplique a zona.
Extraia a parte do ano. Conforme discutido acima, a data pode variar de acordo com o fuso horário. Portanto, o ano pode variar dependendo da zona que você forneceu na linha anterior do código acima!
Obtenha o primeiro dia daquele ano.
Deixe java.time determinar o primeiro momento daquele dia naquela zona. Não presuma que o horário de início é 00:00. Algumas datas em algumas zonas começam em outro horário, como 01:00.
Observe que na determinação do primeiro momento do ano, passamos o argumento for
ZoneId
toatStartOfDay
, em contraste com o seu código. O resultado aqui é umZonedDateTime
em vez de noLocalDateTime
seu código.Por último, convertemos para
java.util.Date
. No código greenfield , evitaríamos essa classe como a Peste. Mas na sua base de código existente, devemos converter para interoperar com as partes do seu código antigo ainda não atualizadas para java.time .MongoDB
Você disse:
Muita coisa para desempacotar aí, com poucos detalhes seus.
Sugiro que você poste outra pergunta especificamente sobre esse assunto. Forneça detalhes suficientes, juntamente com código de exemplo, para fazer um diagnóstico.