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
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 LocalTime
y 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 Locale
por 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 ZONE
todo 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 Instant
un 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.70
o CAD 142.70
o 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:
- Analice esa cadena como un
LocalDateTime
. - Aplique un desplazamiento desde UTC para obtener un
OffsetDateTime
o (mejor) aplicar una zona horaria para obtener unZonedDateTime
.
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 ) ;
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?
- Java SE 8, Java SE 9, Java SE 10, Java SE 11y posteriores: parte de la API de Java estándar con una implementación integrada.
- Java 9 trajo algunas características y correcciones menores.
- Java SE 6 y Java SE 7
- La mayoría de java.tiempo la funcionalidad está adaptada a Java 6 y 7 en ThreeTen-Backport.
- Androide
- Las versiones posteriores de Android (26+) incluyen implementaciones del java.tiempo clases
- Para Android anterior (API desazucarado trae un subconjunto de los java.tiempo funcionalidad no integrada originalmente en Android.
- Si el desazucarado no te ofrece lo que necesitas, el TresDiezABP el proyecto se adapta ThreeTen-Backport (mencionado anteriormente) a Android. Ver Cómo usar ThreeTenABP….
-
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 PostgresTIMESTAMP WITHOUT TIME ZONE
. Por separado, en una segunda columna de tipo de texto, almacena la ID de la zona horaria deseada, comoAmerica/Chicago
. Al crear un calendario para presentarlo al usuario, en tiempo de ejecución extraiga el primero comoLocalDateTime
& 2do comoZoneId
combinar para formarZonedDateTime
.– 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”
¿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 potencialLocalDateTime
generar un momento real comoZonedDateTime
/Instant
.– Basil Bourque
2 de marzo de 2017 a las 0:15