Ignorando del tutto i fusi orari in Rails e PostgreSQL


164

Ho a che fare con date e orari in Rails e Postgres e ho riscontrato questo problema:

Il database è in UTC.

L'utente imposta un fuso orario di scelta nell'app Rails, ma deve essere utilizzato solo per ottenere l'ora locale degli utenti per confrontare i tempi.

L'utente memorizza un'ora, diciamo il 17 marzo 2012, alle 19:00. Non voglio che vengano salvate le conversioni del fuso orario o il fuso orario. Voglio solo che la data e l'ora vengano salvate. In questo modo se l'utente modificasse il proprio fuso orario, mostrerebbe comunque il 17 marzo 2012, alle 19:00.

Uso solo il fuso orario specificato dagli utenti per ottenere i record "prima" o "dopo" l'ora corrente nel fuso orario locale degli utenti.

Attualmente sto usando 'timestamp senza fuso orario' ma quando recupero i record, rails (?) Li converte nel fuso orario nell'app, che non voglio.

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

Poiché i record nel database sembrano essere UTC, il mio trucco è prendere l'ora corrente, rimuovere il fuso orario con 'Date.strptime (str, "% m /% d /% Y")' e quindi fare il mio interroga con quello:

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

Sembra che ci debba essere un modo più semplice per ignorare solo i fusi orari. Qualche idea?

Risposte:


347

Il tipo di dati timestampè il nome breve per timestamp without time zone.
L'altra opzione timestamptzè l'abbreviazione di timestamp with time zone.

timestamptzè il tipo preferito nella famiglia data / ora, letteralmente. È stato typispreferredimpostato pg_type, che può essere rilevante:

Memoria interna ed epoca

Internamente, i timestamp occupano 8 byte di memoria sul disco e nella RAM. È un valore intero che rappresenta il conteggio dei microsecondi dell'epoca di Postgres, 2000-01-01 00:00:00 UTC.

Postgres ha anche una conoscenza integrata del tempo UNIX comunemente usato che conta i secondi dall'epoca UNIX, 1970-01-01 00:00:00 UTC, e lo utilizza nelle funzioni to_timestamp(double precision)o EXTRACT(EPOCH FROM timestamptz).

Il codice sorgente:

* I timestamp, così come i campi h / m / s degli intervalli, sono memorizzati come
* valori int64 con unità di microsecondi. (C'erano una volta  
* valori doppi con unità di secondi.)

E:

/ * Equivalenti alla data giuliana del Giorno 0 nella contabilità Unix e Postgres * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

La risoluzione del microsecondo si traduce in un massimo di 6 cifre frazionarie per secondi.

timestamp

Un valore digitato come dice a Postgres che non viene fornito esplicitamente alcun fuso orario. Si presuppone il fuso orario corrente. Postgres ignora qualsiasi modificatore di fuso orario aggiunto per errore!timestamp [without time zone]

Nessuna ora viene spostata per la visualizzazione. Con la stessa impostazione del fuso orario tutto va bene. Per un diverso fuso orario l'impostazione del significato cambia, ma valore e display rimangono invariati.

timestamptz

La gestione di timestamp with time zoneè leggermente diversa. Cito il manuale qui :

Per timestamp with time zone, il valore memorizzato internamente è sempre in UTC (Universal Coordinated Time ...)

Enorme enfasi sulla mia. Il fuso orario stesso non viene mai memorizzato . È un modificatore di input utilizzato per calcolare il timestamp UTC corrispondente, che viene memorizzato - o e un modificatore di output utilizzato per calcolare l'ora locale da visualizzare - con l'offset del fuso orario aggiunto. Se non si aggiunge un offset per timestamptzl'ingresso, si presume l'impostazione corrente del fuso orario della sessione. Tutti i calcoli vengono eseguiti con valori di data / ora UTC. Se è necessario (o potrebbe essere necessario) gestire più di un fuso orario, utilizzare timestamptz.

I client come psql o pgAdmin o qualsiasi applicazione che comunica tramite libpq (come Ruby con la gemma pg) vengono presentati con il timestamp più l'offset per il fuso orario corrente o in base al fuso orario richiesto (vedi sotto). È sempre lo stesso punto nel tempo , varia solo il formato di visualizzazione. Oppure, come dice il manuale :

Tutte le date e gli orari sensibili al fuso orario sono memorizzati internamente in UTC. Vengono convertiti in ora locale nella zona specificata dal parametro di configurazione TimeZone prima di essere visualizzati sul client.

Considera questo semplice esempio (in psql):

db = # SELECT timestamptz '2012-03-05 20:00 +03 ';
      timestamptz
------------------------
 05-03-2012 18:00:00 +01

Enorme enfasi sulla mia. Cos'è successo qua?
Ho scelto un offset di fuso orario arbitrario +3per l'input letterale. Per Postgres, questo è solo uno dei molti modi per inserire il timestamp UTC 2012-03-05 17:00:00. Il risultato della query viene visualizzato per l'impostazione del fuso orario corrente Vienna / Austria nel mio test, che ha un offset +1durante l'inverno e +2durante l'ora legale: 2012-03-05 18:00:00+01perché cade nell'orario invernale.

Postgres ha già dimenticato come è stato inserito questo valore. Tutto ciò che ricorda è il valore e il tipo di dati. Proprio come con un numero decimale. numeric '003.4', numeric '3.40'O numeric '+3.4'- il tutto risultato nella esattamente lo stesso valore interno.

AT TIME ZONE

Non appena ottieni una comprensione di questa logica, puoi fare tutto quello che vuoi. Tutto ciò che manca ora è uno strumento per interpretare o rappresentare i letterali del timestamp in base a un fuso orario specifico. È qui che AT TIME ZONEentra in gioco il costrutto. Esistono due diversi casi d'uso. timestamptzviene convertito in timestampe viceversa.

Per inserire l'UTC timestamptz 2012-03-05 17:00:00+0:

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

... che equivale a:

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

Per visualizzare lo stesso punto nel tempo di EST timestamp(Eastern Standard Time):

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

Esatto, AT TIME ZONE 'UTC' due volte . Il primo interpreta il timestampvalore come (dato) timestamp UTC che restituisce il tipo timestamptz. Il secondo converte il timestamptzalla timestampnel dato fuso 'EST' - cosa un orologio nelle visualizzazioni dell'ora zona EST a questo punto unico nel tempo.

Esempi

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);

Restituisce 8 (o 9) righe identiche con colonne timestamptz che contengono lo stesso timestamp UTC 2012-03-05 17:00:00. La 9a fila sembra funzionare nel mio fuso orario, ma è una trappola malvagia. Vedi sotto.

① Le righe 6 - 8 con il nome del fuso orario e l' abbreviazione del fuso orario per l'ora delle Hawaii sono soggette all'ora legale (ora legale) e potrebbero essere diverse, sebbene non attualmente. Un nome di fuso orario come 'US/Hawaii'è a conoscenza delle regole dell'ora legale e di tutti i turni storici automaticamente, mentre un'abbreviazione come HSTè solo un codice stupido per un offset fisso. Potrebbe essere necessario aggiungere un'abbreviazione diversa per l'ora legale / solare. Il nome interpreta correttamente qualsiasi timestamp nel fuso orario specificato. Una sigla è a buon mercato, ma ha bisogno di essere quella giusta per il dato timestamp:

L'ora legale non è tra le idee più brillanti mai concepite dall'umanità.

② La riga 9, contrassegnata come una pistola caricata, funziona per me , ma solo per coincidenza. Se espressi esplicitamente un valore letterale timestamp [without time zone], qualsiasi offset del fuso orario viene ignorato ! Viene utilizzato solo il timestamp semplice. Il valore viene quindi forzato automaticamente timestamptznell'esempio per corrispondere al tipo di colonna. Per questo passaggio, timezonesi presume l' impostazione della sessione corrente, che risulta essere lo stesso fuso orario +1nel mio caso (Europa / Vienna). Ma probabilmente non nel tuo caso, il che si tradurrà in un valore diverso. In breve: non lanciare timestamptzletterali timestampo perdere l'offset del fuso orario.

Le tue domande

L'utente memorizza un'ora, diciamo il 17 marzo 2012, alle 19:00. Non voglio che vengano salvate le conversioni del fuso orario o il fuso orario.

Il fuso orario non viene mai memorizzato. Utilizzare uno dei metodi sopra indicati per inserire un timestamp UTC.

Uso solo il fuso orario specificato dagli utenti per ottenere i record "prima" o "dopo" l'ora corrente nel fuso orario locale degli utenti.

È possibile utilizzare una query per tutti i client in diversi fusi orari.
Per un tempo globale assoluto:

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

Per l'ora in base all'orologio locale:

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

Non sei ancora stanco delle informazioni di base? C'è di più nel manuale.


2
Dettagli minori, ma penso che i timestamp siano memorizzati internamente come il numero di microsecondi dal 2000-01-01 - vedere la sezione relativa al tipo di dati data / ora del manuale. Le mie ispezioni sulla fonte sembrano confermarlo. Strano usare un'origine diversa per l'epoca!
dannoso il

2
@harmic Per quanto riguarda l'epoca diversa ... In realtà non è così strano. Questa pagina di Wikipedia elenca due dozzine di epoche utilizzate da vari sistemi informatici. Mentre l' epoca di Unix è comune, non è l'unica.
Basil Bourque,

4
@ErwinBrandstetter Questa è un'ottima risposta, fatta eccezione per un grave difetto. Come ha commentato l'offensivo, Postgres non usa il tempo Unix. Secondo il documento : (a) L'epoca è 2001-01-01 anziché Unix '1970-01-01, e (b) Mentre il tempo Unix ha una risoluzione di interi secondi, Postgres mantiene frazioni di secondi. Il numero di cifre frazionarie dipende dall'opzione del tempo di compilazione: da 0 a 6 quando viene utilizzata la memorizzazione di numeri interi a otto byte (impostazione predefinita) o da 0 a 10 quando viene utilizzata la memorizzazione in virgola mobile (obsoleta).
Basil Bourque,

2
@BasilBourque: sono consapevole di questo sfortunato errore. Se non ti dispiace, sei il benvenuto a modificarlo. Ho visto alcune delle tue risposte in passato e tu sei bravo a farlo. Un'altra mia modifica lo costringerebbe al wiki della comunità - nel corso del tempo ho fatto molti sforzi (e modifiche) per renderlo chiaro e completo.
Erwin Brandstetter,

2
CORREZIONE: Nel mio commento precedente, ho erroneamente citato l'epoca di Postgres come 2001. In realtà è il 2000 .
Basil Bourque,

1

Se vuoi trattare in UTC per impostazione predefinita:

In config/application.rb, aggiungi:

config.time_zone = 'UTC'

Quindi, se si memorizza il nome del fuso orario dell'utente corrente, current_user.timezonesi può dire.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezonedovrebbe essere un nome di fuso orario valido, altrimenti otterrai ArgumentError: Invalid Timezone, vedi l'elenco completo .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.