Ignorando por completo las zonas horarias en Rails y PostgreSQL

164

Estoy tratando con fechas y horas en Rails y Postgres y me encuentro con este problema:

La base de datos está en UTC.

El usuario establece una zona horaria de elección en la aplicación Rails, pero solo se debe usar al obtener la hora local de los usuarios para comparar los tiempos.

El usuario almacena un horario, digamos el 17 de marzo de 2012 a las 7 p.m. No quiero que se almacenen las conversiones de zona horaria o la zona horaria. Solo quiero guardar esa fecha y hora. De esa forma, si el usuario cambiara su zona horaria, aún mostraría el 17 de marzo de 2012 a las 7 p.m.

Solo uso la zona horaria especificada por los usuarios para obtener registros 'antes' o 'después' de la hora actual en la zona horaria local de los usuarios.

Actualmente estoy usando 'marca de tiempo sin zona horaria' pero cuando recupero los registros, los rieles (?) Los convierten a la zona horaria en la aplicación, lo que no quiero.

Appointment.first.time
 => Fri, 02 Mar 2012 19:00:00 UTC +00:00 

Debido a que los registros en la base de datos parecen salir como UTC, mi truco es tomar la hora actual, eliminar la zona horaria con 'Date.strptime (str, "% m /% d /% Y")' y luego hacer mi consulta con eso:

.where("time >= ?", date_start)

Parece que debe haber una manera más fácil de ignorar las zonas horarias. ¿Algunas ideas?

99 millas
fuente

Respuestas:

347

El tipo de datos timestampes el nombre corto de timestamp without time zone.
La otra opción timestamptzes la abreviatura de timestamp with time zone.

timestamptzes el tipo preferido en la familia de fecha / hora, literalmente. Se ha typispreferredestablecido pg_type, que puede ser relevante:

Almacenamiento interno y época

Internamente, las marcas de tiempo ocupan 8 bytes de almacenamiento en el disco y en la RAM. Es un valor entero que representa el recuento de microsegundos de la época de Postgres, 2000-01-01 00:00:00 UTC.

Postgres también tiene un conocimiento incorporado del tiempo de recuento de tiempo UNIX comúnmente utilizado desde la época UNIX, 1970-01-01 00:00:00 UTC, y lo usa en funciones to_timestamp(double precision)o EXTRACT(EPOCH FROM timestamptz).

El código fuente:

* Las marcas de tiempo, así como los campos de intervalos h / m / s, se almacenan como
* valores int64 con unidades de microsegundos. (Érase una vez  
* valores dobles con unidades de segundos.)

Y:

/ * Equivalentes de fecha juliana del día 0 en el cálculo de Unix y Postgres * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

La resolución de microsegundos se traduce en un máximo de 6 dígitos fraccionarios por segundos.

timestamp

Un valor escrito como indica a Postgres que no se proporciona explícitamente ninguna zona horaria. Se asume la zona horaria actual. ¡Postgres ignora cualquier modificador de zona horaria agregado por error!timestamp [without time zone]

No se cambian las horas para mostrar. Con la misma configuración de zona horaria, todo está bien. Para una configuración de zona horaria diferente, el significado cambia, pero el valor y la visualización permanecen igual.

timestamptz

El manejo de timestamp with time zonees sutilmente diferente. Cito el manual aquí :

Para timestamp with time zone, el valor almacenado internamente siempre está en UTC (Tiempo Universal Coordinado ...)

El énfasis en negrita es mío. La zona horaria en sí misma nunca se almacena . Es un modificador de entrada que se usa para calcular la marca de tiempo UTC correspondiente, que se almacena, o un modificador de salida que se usa para calcular la hora local a mostrar, con el desplazamiento de zona horaria adjunto. Si no agrega un desplazamiento para la timestamptzentrada, se supone la configuración de zona horaria actual de la sesión. Todos los cálculos se realizan con valores de marca de tiempo UTC. Si tiene que (o puede que tenga que) lidiar con más de una zona horaria, use timestamptz.

A los clientes como psql o pgAdmin o cualquier aplicación que se comunique a través de libpq (como Ruby con la gema pg) se les presenta la marca de tiempo más el desplazamiento para la zona horaria actual o de acuerdo con una zona horaria solicitada (ver más abajo). Siempre es el mismo punto en el tiempo , solo varía el formato de visualización. O, como dice el manual :

Todas las fechas y horas con reconocimiento de zona horaria se almacenan internamente en UTC. Se convierten a la hora local en la zona especificada por el parámetro de configuración TimeZone antes de mostrarse al cliente.

Considere este ejemplo simple (en psql):

db = # SELECCIONAR marca de tiempo '2012-03-05 20:00 +03 ';
      marca de tiempo
------------------------
 2012-03-05 18:00:00 +01

El énfasis en negrita es mío. ¿Que pasó aquí?
Elegí un desplazamiento de zona horaria arbitrario +3para el literal de entrada. Para Postgres, esta es solo una de las muchas formas de ingresar la marca de tiempo UTC2012-03-05 17:00:00 . El resultado de la consulta se muestra para la zona horaria actual que configura Viena / Austria en mi prueba, que tiene un desplazamiento +1durante el invierno y +2durante el verano: 2012-03-05 18:00:00+01porque cae en invierno.

Postgres ya ha olvidado cómo se ha introducido este valor. Todo lo que recuerda es el valor y el tipo de datos. Al igual que con un número decimal. numeric '003.4', numeric '3.40'o numeric '+3.4'- todos dan como resultado exactamente el mismo valor interno.

AT TIME ZONE

Tan pronto como comprenda esta lógica, puede hacer lo que quiera. Todo lo que falta ahora es una herramienta para interpretar o representar literales de marca de tiempo de acuerdo con una zona horaria específica. Ahí es donde AT TIME ZONEentra en juego la construcción. Hay dos casos de uso diferentes. timestamptzse convierte timestampy viceversa.

Para ingresar al UTC timestamptz 2012-03-05 17:00:00+0 :

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

... que es equivalente a:

SELECT timestamptz '2012-03-05 17:00:00 UTC'

Para mostrar el mismo punto en el tiempo que EST timestamp (hora estándar del este):

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

Así es, AT TIME ZONE 'UTC' dos veces . El primero interpreta eltimestamp valor como (dado) la marca de tiempo UTC que devuelve el tipo timestamptz. El segundo convierte el timestamptza timestampen la zona horaria dada 'EST', lo que muestra un reloj en la zona horaria EST en este punto único en el tiempo.

Ejemplos

SELECT ts AT TIME ZONE 'UTC'
FROM  (
   VALUES
      (1, timestamptz '2012-03-05 17:00:00+0')
    , (2, timestamptz '2012-03-05 18:00:00+1')
    , (3, timestamptz '2012-03-05 17:00:00 UTC')
    , (4, timestamp   '2012-03-05 11:00:00'  AT TIME ZONE '+6') 
    , (5, timestamp   '2012-03-05 17:00:00'  AT TIME ZONE 'UTC') 
    , (6, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'US/Hawaii')  -- 
    , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii')                  -- 
    , (8, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'HST')        -- 
    , (9, timestamp   '2012-03-05 18:00:00+1')  --  loaded footgun!
      ) t(id, ts);

Devuelve 8 (o 9) filas idénticas con columnas de marca de tiempo que contienen la misma marca de tiempo UTC 2012-03-05 17:00:00. La novena fila funciona en mi zona horaria, pero es una trampa malvada. Vea abajo.

① Las filas 6 - 8 con el nombre de la zona horaria y la abreviatura de la zona horaria para la hora de Hawái están sujetas a DST (horario de verano) y pueden diferir, aunque no actualmente. Un nombre de zona horaria como 'US/Hawaii'conoce las reglas de DST y todos los cambios históricos automáticamente, mientras que una abreviatura como HSTes solo un código tonto para un desplazamiento fijo. Es posible que deba agregar una abreviatura diferente para el horario de verano / estándar. El nombre interpreta correctamente cualquier marca de tiempo en la zona horaria dada. Una abreviatura es barata, pero debe ser la correcta para la marca de tiempo dada:

El horario de verano no se encuentra entre las ideas más brillantes que la humanidad haya tenido.

② La fila 9, marcada como pistola cargada, funciona para mí , pero solo por coincidencia. Si expulsa explícitamente un literal a timestamp [without time zone], ¡ se ignora cualquier desplazamiento de zona horaria ! Solo se utiliza la marca de tiempo desnuda. El valor se coacciona automáticamente timestamptzen el ejemplo para que coincida con el tipo de columna. Para este paso, timezonese supone la configuración de la sesión actual, que resulta ser la misma zona horaria +1en mi caso (Europa / Viena). Pero probablemente no en su caso, lo que dará como resultado un valor diferente. En resumen: no emita timestamptzliterales timestampo perderá el desplazamiento de la zona horaria.

Tus preguntas

El usuario almacena un horario, digamos el 17 de marzo de 2012 a las 7 p.m. No quiero que se almacenen las conversiones de zona horaria o la zona horaria.

La zona horaria en sí misma nunca se almacena. Use uno de los métodos anteriores para ingresar una marca de tiempo UTC.

Solo uso la zona horaria especificada por los usuarios para obtener registros 'antes' o 'después' de la hora actual en la zona horaria local de los usuarios.

Puede usar una consulta para todos los clientes en diferentes zonas horarias.
Por tiempo global absoluto:

SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

Por hora según el reloj local:

SELECT * FROM tbl WHERE time_col > now()::time

¿Todavía no estás cansado de la información de fondo? Hay más en el manual.

Erwin Brandstetter
fuente
2
Detalles menores, pero creo que las marcas de tiempo se almacenan internamente como la cantidad de microsegundos desde 2000-01-01 - vea la sección de tipo de datos de fecha / hora del manual. Mis propias inspecciones de la fuente parecen confirmarlo. ¡Es extraño usar un origen diferente para la época!
Harmic
2
@harmic En cuanto a diferentes épocas ... En realidad no es tan extraño. Esta página de Wikipedia enumera dos docenas de épocas utilizadas por diversos sistemas informáticos. Si bien la época de Unix es común, no es la única.
Basil Bourque
44
@ErwinBrandstetter Esta es una gran respuesta, excepto por una falla grave. Como comentó Harmic, Postgres no usa el tiempo Unix. Según el documento : (a) La época es 2001-01-01 en lugar de Unix '1970-01-01, y (b) Si bien el tiempo Unix tiene una resolución de segundos enteros, Postgres mantiene fracciones de segundos. El número de dígitos fraccionarios depende de la opción de tiempo de compilación: de 0 a 6 cuando se usa el almacenamiento de enteros de ocho bytes (predeterminado), o de 0 a 10 cuando se usa el almacenamiento de punto flotante (en desuso).
Basil Bourque
2
@BasilBourque: Soy consciente de este desafortunado error. Si no le importa, puede editarlo. He visto algunas de tus respuestas en el pasado y eres bueno en eso. Una edición más mía obligaría a esto a la wiki de la comunidad: con el tiempo me he esforzado (y editado) para que sea claro y completo.
Erwin Brandstetter
2
CORRECCIÓN: En mi comentario anterior, cité incorrectamente la época de Postgres como 2001. En realidad es 2000 .
Basil Bourque
1

Si desea negociar en UTC por defecto:

En config/application.rb, agregue:

config.time_zone = 'UTC'

Luego, si almacena el nombre de la zona horaria del usuario actual current_user.timezone, puede decirlo.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezonedebe ser un nombre válido de zona horaria, de lo contrario obtendrá ArgumentError: Invalid Timezone, vea la lista completa .

dorio
fuente