Almacenar citas en una base de datos SQL como Postgres para usar con java.time framework
Digamos que tenemos una cita en Milán Italia para el 23/01/2021 21:00 "Europa / Roma". Esta cita se guarda en la base de datos en UTC en una columna de un tipo similar al tipo estándar de SQL TIMESTAMP WITH TIME ZONE.
Ahora, un usuario que vive en Nueva York, EE. UU., Debe saber cuándo se llevará a cabo esta cita. Podemos mostrarle al usuario la fecha y hora convertida a la zona horaria "America / New_York", o en su lugar, a la TZ "Europa / Roma". Una vez que el usuario vuele de Nueva York a Milán, encontrará ambas informaciones útiles.
El punto es almacenar todo lo convertido en la misma referencia TZ (UTC) y manipular la fecha y hora según el objetivo que tenga utilizando el marco java.time incluido con Java moderno.
¿Tiene sentido o hay algo mal / falta?
Respuestas
¿Tiene sentido o hay algo mal / falta?
Depende del tipo de cita.
Hay dos tipos de citas:
- Un momento, un punto específico en la línea de tiempo, ignorando cualquier cambio en las reglas de la zona horaria.
Ejemplo: lanzamiento de un cohete. - Una fecha y hora del día que deben ajustarse a los cambios en las reglas de zona horaria.
Ejemplo: visita médica / dental.
Momento
Si estamos reservando el lanzamiento de un cohete, por ejemplo, no nos importa la fecha y la hora del día. Solo nos importa el momento en que (a) los cielos se alineen y (b) esperamos un clima favorable.
Si en el tiempo intermedio los políticos cambian las reglas de la zona horaria en uso en nuestro sitio de lanzamiento o en nuestras oficinas, eso no tiene ningún efecto en nuestra cita de lanzamiento. Si los políticos que gobiernan nuestro sitio de lanzamiento adoptan el horario de verano (DST) , el momento de nuestro lanzamiento sigue siendo el mismo. Si los políticos que gobiernan nuestras oficinas deciden cambiar el reloj media hora antes debido a las relaciones diplomáticas con un país vecino, el momento de nuestro lanzamiento sigue siendo el mismo.
Para tal cita, sí, su enfoque sería el correcto. Registraría la cita en UTC utilizando una columna de tipo TIMESTAMP WITH TIME ZONE. Una vez recuperado, ajústelo a cualquier zona horaria que prefiera el usuario.
Las bases de datos como Postgres usan cualquier información de zona horaria que acompaña a una entrada para ajustarse a UTC y luego desechan esa información de zona horaria. Cuando recupera el valor de Postgres, siempre representará una fecha con la hora del día como se ve en UTC. Tenga cuidado, algunas herramientas o middleware pueden tener la característica de aplicar una zona horaria predeterminada entre la recuperación de la base de datos y la entrega al programador. Pero sea claro: Postgres siempre guarda y recupera valores de tipo TIMESTAMP WITH TIME ZONEen UTC, siempre UTC y offset-from-UTC de cero horas-minutos-segundos.
Aquí hay un ejemplo 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 el mismo momento en UTC, conviértalo a Instant. Un Instantobjeto siempre representa un momento visto en UTC.
Instant launchInstant = launchMomentAsSeenInRome.toInstant() ; // Adjust from Rome time zone to UTC.
launchInstant.toString (): 2021-01-23T20: 00: 00Z
El Zal final del ejemplo de cadena anterior es la notación estándar para UTC y se pronuncia "Zulu".
Desafortunadamente, el equipo de JDBC 4.2 se olvidó de requerir soporte para los tipos Instanto ZonedDateTime. Por lo tanto, su controlador JDBC puede o no leer / escribir dichos objetos en su base de datos. Si no es así, simplemente conviértalo a OffsetDateTime. Los tres tipos representan un momento, un punto específico en la línea de tiempo. Pero OffsetDateTimetiene el soporte requerido por JDBC 4.2 por razones que se me escapan.
OffsetDateTime odtLaunchAsSeenInRome = launchMomentAsSeenInRome.toOffsetDateTime() ;
Escribiendo en la base de datos.
myPreparedStatement.setObject( … , odtLaunchAsSeenInRome ) ;
Recuperación de la base de datos.
OffsetDateTime launchMoment = myResultSet.getObject( … , OffsetDateTime.class ) ;
Ajuste a la zona horaria de Nueva York deseada por su usuario.
ZoneId zoneAmericaNewYork = ZoneId.of( "America/New_York" ) ;
ZonedDateTime launchAsSeenInNewYork = launchMoment.atZoneSameInstant( zoneAmericaNewYork ) ;
launchAsSeenInNewYork.toString (): 2021-01-23T15: 00-05: 00 [America / New_York]
Puede ver todo el código anterior ejecutado en vivo en IdeOne.com .
Por cierto, el seguimiento de eventos pasados también se trata como un momento. ¿Cuándo llegó el paciente a la cita, cuándo pagó el cliente la factura, cuándo un nuevo empleado firmó sus documentos, cuándo se bloqueó el servidor ... todo esto se registra como un momento en UTC. Como se mencionó anteriormente, normalmente eso sería un Instant, aunque ZonedDateTimey OffsetDateTimetambién representaría un momento. Para el uso de la base de datos TIMESTAMP WITH TIME ZONE(no WITHOUT).
Hora del día
Espero que la mayoría de las aplicaciones orientadas a los negocios se centren en citas del otro tipo, donde apuntamos a una fecha con una hora del día en lugar de un momento específico.
Si el usuario programa una cita con su proveedor de atención médica para revisar los resultados de una prueba, lo hace para una hora determinada en esa fecha. Si mientras tanto los políticos cambian las reglas de su zona horaria, adelantando o atrasando el reloj una hora o media hora o cualquier otra cantidad de tiempo, la fecha y la hora del día de esa cita médica siguen siendo las mismas. En realidad, el punto de la línea de tiempo de la cita original se cambiará después de que los políticos cambien la zona horaria, pasando a un punto anterior / posterior en la línea de tiempo.
Para tales citas, no almacenamos la fecha y la hora del día como se ve en UTC. Nosotros no usamos el tipo de columna de base de datos TIMESTAMP WITH TIME ZONE.
Para tales citas, almacenamos la fecha con la hora del día sin tener en cuenta la zona horaria. Usamos una columna de base de datos de tipo TIMESTAMP WITHOUT TIME ZONE(aviso en WITHOUTlugar de WITH). El tipo coincidente en Java es LocalDateTime.
LocalDate medicalApptDate = LocalDate.of( 2021 , 1 , 23 ) ;
LocalTime medicalApptTime = LocalTime.of( 21 , 0 ) ;
LocalDateTime medicalApptDateTime = LocalDateTime.of( medicalApptDate , medicalApptTime ) ;
Escribe eso en la base de datos.
myPreparedStatement.setObject( … , medicalApptDateTime ) ;
Sea claro en esto: un LocalDateTimeobjeto no representa un momento, no es un punto específico en la línea de tiempo. Un LocalDateTimeobjeto representa un rango de momentos posibles a lo largo de aproximadamente 26 a 27 horas de la línea de tiempo (el rango de zonas horarias alrededor del mundo). Para dar un significado real a a LocalDateTime, debemos asociar una zona horaria prevista.
Para esa zona horaria prevista, utilice una segunda columna para almacenar el identificador de zona. Por ejemplo, las cadenas Europe/Romeo America/New_York. Consulte la lista de nombres de zonas .
ZoneId zoneEuropeRome = ZoneId.of( "Europe/Rome" ) ;
Escríbalo en la base de datos como texto.
myPreparedStatement.setString( … , zoneEuropeRome ) ;
Recuperación. Recupere el nombre de la zona como texto y cree una instancia de un ZoneIdobjeto.
LocalDateTime medicalApptDateTime = myResultSet.getObject( … , LocalDateTime.class ) ;
ZoneId medicalApptZone = ZoneId.of( myResultSet.getString( … ) ) ;
Junte esas dos piezas para determinar un momento representado como un ZonedDateTimeobjeto. Haga esto de forma dinámica cuando necesite programar un calendario. Pero no guardes el momento. Si los políticos redefinen la (s) zona (s) horaria (s) en el futuro, entonces se debe calcular un momento diferente.
ZonedDateTime medicalApptAsSeenInCareProviderZone = ZonedDateTime.of( medicalApptDateTime , medicalApptZone ) ;
El usuario viaja a Nueva York, EE. UU. Necesitan saber cuándo llamar al proveedor de atención médica en Milán, Italia, de acuerdo con los relojes en la pared en su ubicación temporal de Nueva York. Así que ajústese de una zona horaria a otra. Mismo momento, diferente hora del reloj de pared.
ZoneId zoneAmericaNewYork = ZoneId.of( "America/New_York" ) ;
ZonedDateTime medicalApptAsSeenInNewYork = medicalApptAsSeenInCareProviderZone.withZoneSameInstant( zoneAmericaNewYork ) ;
tzdata
Tenga en cuenta que si las reglas de las zonas horarias deseadas pueden estar cambiando, debe actualizar la copia de las definiciones de las zonas horarias en sus computadoras.
Java contiene su propia copia de tzdata , al igual que el motor de base de datos de Postgres. Y su sistema operativo host también. Este código en particular que se muestra aquí solo requiere que Java esté actualizado. Si usa Postgres para realizar ajustes de zona horaria, su tzdata también debe estar actualizado. Y para el registro y demás, el sistema operativo de su host debe mantenerse actualizado. Para que el usuario pueda ver correctamente el reloj, el sistema operativo de su máquina cliente también debe estar actualizado.
Cuidado: los políticos de todo el mundo han mostrado una inclinación por cambiar sus zonas horarias con una frecuencia sorprendente y, a menudo, con poca advertencia previa.
Acerca de java.time
El marco java.time está integrado en Java 8 y versiones posteriores. Estas clases suplantar la vieja problemáticos heredados clases de fecha y hora como java.util.Date, Calendar, y SimpleDateFormat.
Para obtener más información, consulte el tutorial de Oracle . Y busque Stack Overflow para obtener muchos ejemplos y explicaciones. La especificación es JSR 310 .
El proyecto Joda-Time , ahora en modo de mantenimiento , aconseja la migración a las clases java.time .
Puede intercambiar objetos java.time directamente con su base de datos. Utilice un controlador JDBC compatible con JDBC 4.2 o posterior. No se necesitan cuerdas, no se necesitan java.sql.*clases. Hibernate 5 y JPA 2.2 admiten java.time .
¿Dónde obtener las clases java.time?
- Java SE 8 , Java SE 9 , Java SE 10 , Java SE 11 y posterior: 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 parte de la funcionalidad de java.time está adaptada a Java 6 y 7 en ThreeTen-Backport .
- Androide
- Versiones posteriores de Android (26+) incluyen implementaciones de las clases java.time .
- Para Android anterior (<26), un proceso conocido como API desugaring trae un subconjunto de la funcionalidad java.time que originalmente no estaba integrada en Android.
- Si el desugaring no ofrece lo que necesita, el ThreeTenABP proyecto se adapta ThreeTen-Backport (mencionado anteriormente) para Android. Consulte Cómo utilizar ThreeTenABP… .
Una solución muy sencilla sería dejar la conversión a PostgreSQL. Si configura el timezoneparámetro correctamente para cada sesión y usa timestamp with time zonePostgreSQL, se mostrará automáticamente la marca de tiempo al usuario de Nueva York en la hora de Nueva York.