LocalDateTime a ZonedDateTime

10 minutos de lectura

avatar de usuario de sonoerin
sonoerín

Tengo la aplicación web Java 8 Spring que admitirá varias regiones. Necesito hacer eventos de calendario para la ubicación de un cliente. Entonces, digamos que mi servidor web y Postgres está alojado en la zona horaria MST (pero supongo que podría estar en cualquier lugar si vamos a la nube). Pero el cliente está en EST. Entonces, siguiendo algunas de las mejores prácticas que leí, pensé en almacenar todas las fechas y horas en formato UTC. Todos los campos de fecha y hora en la base de datos se declaran como TIMESTAMP.

Así es como tomo un LocalDateTime y lo convierto a UTC:

ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(dto.getStartDateTime(), ZoneOffset.UTC, null);
//appointment startDateTime is a LocalDateTime
appointment.setStartDateTime( startZonedDT.toLocalDateTime() );

Ahora, por ejemplo, cuando aparece una búsqueda de una fecha, tengo que convertir la hora de la fecha solicitada a UTC, obtener los resultados y convertir a la zona horaria del usuario (almacenada en la base de datos):

ZoneId  userTimeZone = ZoneId.of(timeZone.getID());
ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(appointment.getStartDateTime(), userTimeZone, null);
dto.setStartDateTime( startZonedDT.toLocalDateTime() );

Ahora, no estoy seguro de que este sea el enfoque correcto. También me pregunto si debido a que voy de LocalDateTime a ZonedDateTime y viceversa, podría estar perdiendo información sobre la zona horaria.

Esto es lo que estoy viendo y no me parece correcto. Cuando recibo LocalDateTime de la interfaz de usuario, obtengo esto:

2016-04-04T08:00

ZonedDateTime =

dateTime=2016-04-04T08:00
offset="Z"
zone="Z"

Y luego, cuando asigno ese valor convertido a mi cita LocalDateTime, almaceno:

2016-04-04T08:00

Siento que porque estoy almacenando en LocalDateTime estoy perdiendo la zona horaria que convertí en ZonedDateTime.

¿Debo hacer que mi entidad (cita) use ZonedDateTime en lugar de LocalDateTime para que Postgres no pierda esa información?

—————- Editar —————-

Después de la excelente respuesta de Basil, me di cuenta de que tengo el lujo de no preocuparme por la zona horaria de los usuarios: todas las citas están en una ubicación específica, por lo que puedo almacenar todas las fechas como UTC y luego convertirlas a la zona horaria de la ubicación cuando las recupero. Hice la siguiente pregunta de seguimiento

  • ¿Tu aplicación necesita información sobre la zona horaria?

    – Sanj

    5 de abril de 2016 a las 5:03

  • Con respecto a su sección “Editar”, sepa que los políticos amar para redefinir las zonas horarias, las compensaciones, el horario de verano (DST), etc. Así que nunca vaya también muy lejos en el futuro con valores UTC. Si se refiere a citas de higiene dental hechas con mucha anticipación, ej. “3 PM el 5 de diciembre de 2017 a finales de este año”, entonces es posible que desee utilizar un LocalDateTime para almacenar ese hecho. La intención allí es a las 3 p. m., sin embargo, los políticos pueden haber redefinido ese momento para entonces. Para un horario, puede aplicar temporalmente la zona horaria al potencial LocalDateTime generar un momento real como ZonedDateTime/Instant.

    – Basil Bourque

    2 de marzo de 2017 a las 0:15


Avatar de usuario de Basil Bourque
albahaca bourque

Postgres no tiene tal tipo de datos como TIMESTAMP. postgres tiene dos tipos para la fecha más la hora del día: TIMESTAMP WITH TIME ZONE y TIMESTAMP WITHOUT TIME ZONE. Estos tipos tienen un comportamiento muy diferente con respecto a la información de la zona horaria.

  • El WITH tipo utiliza cualquier información de desplazamiento o zona horaria para ajustar la fecha y hora a UTC, luego se deshace de ese desplazamiento o zona horaria; Postgres nunca guarda la información de compensación/zona.
    • Este tipo representa un momento, un punto específico en la línea de tiempo.
  • El WITHOUT tipo ignora cualquier información de desplazamiento o zona que pueda estar presente.
    • Este tipo hace no representar un momento. Representa una vaga idea de potencial momentos a lo largo de un rango de aproximadamente 26-27 horas (el rango de zonas horarias alrededor del mundo).

Prácticamente siempre quieres la WITH tipo, como explicado aquí por el experto David E. Wheeler. El WITHOUT solo tiene sentido cuando tienes la vaga idea de una fecha y hora en lugar de un punto fijo en la línea de tiempo. Por ejemplo, “La Navidad de este año comienza en 2016-12-25T00:00:00” se almacenaría en el WITHOUT como se aplica a cualquier zona horaria, que aún no se ha aplicado a ninguna zona horaria única para obtener un momento real en la línea de tiempo. Si los duendes de Papá Noel estuvieran rastreando la hora de inicio de Eugene Oregon EE. UU., entonces usarían el WITH tipo y una entrada que incluía un desplazamiento o zona horaria como 2016-12-25T00:00:00-08:00 que se guarda en Postgres como 2016-12-25T08:00.00Z (donde el Z significa zulú o UTC).

El equivalente de Postgres TIMESTAMP WITHOUT TIME ZONE en java.el tiempo es java.time.LocalDateTime. Como tu intención era trabajar en UTC (algo bueno), deberías no estar usando LocalDateTime (una cosa mala). Ese puede ser el punto principal de confusión y problemas para usted. Sigues pensando en usar LocalDateTime o ZonedDateTime pero no deberías usar ninguno; en su lugar, deberías estar usando Instant (se discute más adelante).

También me pregunto si debido a que voy de LocalDateTime a ZonedDateTime y viceversa, podría estar perdiendo información sobre la zona horaria.

De hecho son. Todo el punto de LocalDateTime Es para perder información de la zona horaria. Por lo tanto, rara vez usamos esta clase en la mayoría de las aplicaciones. De nuevo, el ejemplo de Navidad. U otro ejemplo, “Política de la empresa: todas nuestras fábricas en todo el mundo almuerzan a las 12:30 p. m.”. Eso sería LocalTimey para una fecha en particular, LocalDateTime. Pero eso no tiene un significado real, no es un punto real en la línea de tiempo, hasta que aplica una zona horaria para obtener un ZonedDateTime. Esa pausa para el almuerzo será en puntos diferentes en la línea de tiempo en la fábrica de Delhi que en la fábrica de Düsseldorf y diferente nuevamente en la fábrica de Detroit.

La palabra “local” en LocalDateTime puede ser contrario a la intuición, ya que significa sin particular localidad. Cuando lea “Local” en el nombre de una clase, piense “Ni un momento … no en la línea de tiempo … solo una idea confusa sobre una especie de fecha y hora”.

Sus servidores casi siempre deben estar configurados en UTC en la zona horaria de su sistema operativo. Pero su programación nunca debe depender de esta externalidad, ya que es demasiado fácil para un administrador de sistemas cambiarla o para cualquier otra aplicación Java cambiar la zona horaria predeterminada actual dentro de la JVM. Así que siempre especifique su zona horaria deseada/esperada. (Lo mismo ocurre con Localepor cierto.)

Resultado:

  • Estás trabajando demasiado duro.
  • Los programadores/administradores de sistemas deben aprender a “pensar globalmente, presentar localmente”.

Durante el día de trabajo mientras usa su casco geek, piense en UTC. Solo al final del día, cuando cambie a su laico, debe volver a pensar en la hora local de su ciudad.

Su lógica comercial debe centrarse en UTC. El almacenamiento de su base de datos, la lógica comercial, el intercambio de datos, la serialización, el registro y su propio pensamiento deben realizarse en la zona horaria UTC (y, por cierto, en un reloj de 24 horas). Al presentar datos a los usuarios, solo entonces aplique una zona horaria particular. Piense en las fechas y horas divididas en zonas como algo externo, no como una parte funcional de las funciones internas de su aplicación.

En el lado de Java, use java.time.Instant (un momento en la línea de tiempo en UTC) en gran parte de su lógica comercial.

Instant now = Instant.now();

Con un poco de suerte JDBC los controladores eventualmente se actualizarán para manejar tipos de java.time como Instant directamente. Hasta entonces debemos usar tipos java.sql. La antigua clase java.sql tiene nuevos métodos para la conversión a/desde java.time.

java.sql.TimeStamp ts = java.sql.TimeStamp.valueOf( instant );

Ahora pasa eso java.sql.TimeStamp objeto a través de setTimestamp en un PreparedStatement para ser guardado en una columna definida como TIMESTAMP WITH TIME ZONE en Postgres.

Para ir en la otra dirección:

Instant instant = ts.toInstant();

Eso es fácil, pasando de Instant a java.sql.Timestamp a TIMESTAMP WITH TIME ZONEtodo en UTC. No hay zonas horarias involucradas. La zona horaria predeterminada actual de su sistema operativo de servidor, su JVM y sus clientes es irrelevante.

Para presentar al usuario, aplique una zona horaria. Usar nombres de zona horaria adecuadosnunca los códigos de 3-4 letras como EST o IST.

ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

Puede ajustarse a una zona diferente según sea necesario.

ZonedDateTime zdtKolkata = zdt.withZoneSameInstant( ZoneId.of( "Asia/Kolkata" ) );

Para volver a un Instantun momento en la línea de tiempo en UTC, puede extraer de la ZonedDateTime.

Instant instant = zdt.toInstant();

En ningún lugar allí usamos LocalDateTime.

Si obtiene una parte de los datos sin ningún desplazamiento de UTC o zona horaria, como 2016-04-04T08:00, esos datos son completamente inútiles para usted (suponiendo que no estemos hablando de los escenarios de tipo Navidad o Almuerzo de empresa discutidos anteriormente). Una fecha y hora sin información de compensación/zona es como una cantidad monetaria sin indicar la moneda: 142.70 o incluso $142.70 — inútil. Pero USD 142.70o CAD 142.70o MXN 142.70… esos son útiles.

si consigues eso 2016-04-04T08:00 valor, y usted es absolutamente cierto del contexto previsto de desplazamiento/zona, entonces:

  1. Analice esa cadena como un LocalDateTime.
  2. Aplique un desplazamiento desde UTC para obtener un OffsetDateTimeo (mejor) aplicar una zona horaria para obtener un ZonedDateTime.

Me gusta este código.

LocalDateTime ldt = LocalDateTime.parse( "2016-04-04T08:00" );
ZoneId zoneId = ZoneId.of( "Asia/Kolkata" ); // Or "America/Montreal" etc.
ZonedDateTime zdt = ldt.atZone( zoneId ); // Or atOffset( myZoneOffset ) if only an offset is known rather than a full time zone.

Su pregunta realmente es un duplicado de muchas otras. Estos temas se han discutido muchas veces en otras Preguntas y Respuestas. Le insto a que busque y estudie Stack Overflow para obtener más información sobre este tema.

JDBC 4.2

A partir de JDBC 4.2 podemos intercambiar directamente java.tiempo objetos con la base de datos. No hay necesidad de usar java.sql.Timestamp de nuevo, ni sus clases relacionadas.

almacenar, usar OffsetDateTime como se define en la especificación JDBC.

myPreparedStatement.setObject( … , instant.atOffset( ZoneOffset.UTC ) ) ;  // The JDBC spec requires support for `OffsetDateTime`. 

…o posiblemente usar Instant directamente, si lo admite su controlador JDBC.

myPreparedStatement.setObject( … , instant ) ;  // Your JDBC driver may or may not support `Instant` directly, as it is not required by the JDBC spec. 

recuperando

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

Tabla de tipos de fecha y hora en Java (tanto heredado como moderno) y en SQL estándar


Acerca de java.tiempo

El java.tiempo framework está integrado en Java 8 y versiones posteriores. Estas clases suplantan a las viejas y problemáticas legado clases de fecha y hora como java.util.Date, Calendar& SimpleDateFormat.

Para obtener más información, consulte la Tutorial de oráculo. Y busque Stack Overflow para obtener muchos ejemplos y explicaciones. La especificación es JSR 310.

El Joda-Tiempo proyecto, ahora en modo de mantenimientoaconseja la migración a la java.tiempo clases

puedes intercambiar java.tiempo objetos directamente con su base de datos. Usar una controlador JDBC conforme con JDBC 4.2 o después. No hay necesidad de cuerdas, no hay necesidad de java.sql.* clases Compatibilidad con Hibernate 5 y JPA 2.2 java.tiempo.

¿Dónde obtener las clases java.time?

Tabla de qué biblioteca java.time usar con qué versión de Java o Android

  • Muchas gracias por la excelente respuesta. Había leído muchas publicaciones y artículos sobre el mejor enfoque para manejar las zonas horarias en una aplicación web. Tengo el lujo de saber que todas las horas utilizadas son para una ubicación específica, independientemente de dónde diga que está el reloj del sistema del usuario. Tengo una pregunta de seguimiento sobre mi pregunta, si no le importa. Nuevamente, gracias por una respuesta tan detallada y útil.

    – sonoerín

    07/04/2016 a las 12:41

  • Debo leer esto de nuevo cuando tenga tiempo. Muy buena explicación. Entonces, LocalTime, por ejemplo, indica el valor puro de “HORA”, como un letrero en la puerta de una tienda que indica “Estamos abiertos de 8:00 a 19:00” ← a esto no le importa el horario de verano, por ejemplo, 8 es 8 en todos los días del año para el dueño de la tienda y los clientes. Mientras que cuando necesitamos almacenar una referencia a un evento, debemos enviar su hora con la zona horaria a PostgresSQL; lo guardará en UTC. Finalmente, cuando nosotros u otro usuario necesitemos ver esta hora, se tomará en UTC y se adaptará a la zona horaria del usuario final… ¿es correcto?

    – financiador7

    23 oct 2020 a las 22:36

  • @funder7 (A) Sí, LocalTime representa una hora del día, nada más, nada menos. (B) Reservar un evento o cita en el futuro es un poco complicado, para tener en cuenta posibles cambios en las reglas de la zona. Si quiere decir “cita con el dentista el próximo 21 de enero de 2021 a las 3 p. m.”, almacene la fecha y la hora en tipo Postgres TIMESTAMP WITHOUT TIME ZONE. Por separado, en una segunda columna de tipo de texto, almacena la ID de la zona horaria deseada, como America/Chicago. Al crear un calendario para presentarlo al usuario, en tiempo de ejecución extraiga el primero como LocalDateTime & 2do como ZoneIdcombinar para formar ZonedDateTime.

    – Basil Bourque

    24 de octubre de 2020 a las 4:16


  • Continuemos esta discusión en el chat.

    – Basil Bourque

    24 oct 2020 a las 16:25

  • @funder7 Suponga que reserva su próxima cita dental para el 15 de octubre del año siguiente. Cuando reservó la cita, su país observaba el horario de verano (DST) a partir del último día de septiembre. Y luego su país decide cambiar la regla para que el horario de verano comience el último día de octubre. ¿A qué hora del día debe presentarse en el consultorio del dentista? Una cita registrada en UTC sería incorrecta, desactivada por la hora de compensación del horario de verano.

    – Basil Bourque

    24 oct 2020 a las 16:33


TLDR:

Para convertir de LocalDateTime a ZonedDateTime el siguiente código. Puedes usar .atZone( zoneId )una lista completa de zoneID se encuentra en la columna TZ database name en Wikipedia.

LocalDateTime localDateTime = LocalDateTime.parse( "2016-04-04T08:00" );
ZoneId zoneId = ZoneId.of( "UTC" ); // Or "Asia/Kolkata" etc.
ZonedDateTime zdt = localDateTime.atZone( zoneId );

public ZonedDateTime transformToZonedDateTime(String Date, String timeZone) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
    LocalDate localDate = LocalDate.parse(Date, formatter);
    LocalDateTime localDateTime = localDate.atTime(LocalTime.now());
    ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneOffset.UTC);
    ZoneId zoneId = ZoneId.of(timeZone);
    return zonedDateTime.withZoneSameInstant(zoneId);
}

APORTE: fecha de entrega = “18-05-2023” y zona horaria = “Asia/Hong_Kong”

DEVOLUCIONES: “2023-05-19T20:43Z”

¿Ha sido útil esta solución?