ruby on rails - Ignorando completamente i fusi orari in Rails e PostgreSQL




ruby-on-rails ruby-on-rails-3 (2)

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 quando si ottiene l'ora locale degli utenti per il confronto delle ore.

L'utente memorizza un tempo, 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 ha cambiato il proprio fuso orario, sarebbe comunque visibile 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 utilizzando "data / ora senza fuso orario", ma quando recupero i record, i binari (?) Li convertono nel fuso orario dell'app, che non desidero.

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

Poiché i record nel database sembrano uscire come UTC, il mio hack è prendere l'ora corrente, rimuovere il fuso orario con 'Date.strptime (str, "% m /% d /% Y")' e poi esegui il mio interrogare con quello:

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

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


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

timestamptz è il tipo preferito nella famiglia data / ora, letteralmente. Ha set typispreferred in pg_type , che può essere rilevante:

Epoca

Internamente , i timestamp vengono memorizzati come conteggio da epoch . Postgres utilizza un'epoca del primo momento del primo giorno dell'anno 2000 in UTC, ovvero 2000-01-01T00: 00: 00Z. Otto octets vengono utilizzati per memorizzare il numero di conteggio. A seconda dell'opzione di compilazione, quel numero può essere:

  • Un numero intero a 8 byte (predefinito), con da 0 a 6 cifre di un secondo frazionario
  • Un numero in virgola mobile (deprecato), con da 0 a 10 cifre di un secondo frazionario, in cui la precisione degrada rapidamente per valori più lontani dall'epoca.
    Le installazioni di Postgres moderne usano il numero intero a 8 byte.

Nota che Postgres non usa il tempo Unix . L'epoca di Postgres è il primo momento del 2000-01-01 piuttosto che Unix del 1970-01-01. Mentre il tempo di Unix ha una risoluzione di interi secondi, Postgres mantiene frazioni di secondi.

timestamp

Se definisci un timestamp tipo di dati [without time zone] stai dicendo a Postgres: "Non sto fornendo un fuso orario esplicitamente, supponiamo invece il fuso orario corrente Postgres salva il timestamp così com'è - ignorando un modificatore del fuso orario se dovessi aggiungere uno!

Quando successivamente visualizzi il timestamp , timestamp ciò che hai inserito alla lettera. Con la stessa impostazione del fuso orario tutto va bene. Se l'impostazione del fuso orario per la sessione cambia, così cambia anche il significato del timestamp : il valore rimane lo stesso.

timestamptz

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

Per il timestamp with time zone , il valore memorizzato internamente è sempre in UTC (tempo coordinato universale ...)

Grassetto enfasi mio. Il fuso orario stesso non viene mai memorizzato . Si tratta di un modificatore di input utilizzato per calcolare il timestamp UTC corrispondente, che viene memorizzato - o e il modificatore di output utilizzato per calcolare l'ora locale da visualizzare - con l'offset di fuso orario aggiunto. Se non si aggiunge un offset per timestamptz sull'input, si presume l'impostazione del fuso orario corrente della sessione. Tutti i calcoli sono fatti con i valori di timestamp UTC. Se devi (o potrebbe dover) gestire più di un fuso orario, usa 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 secondo un fuso orario richiesto (vedere sotto). È sempre lo stesso momento , solo il formato di visualizzazione varia. 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
------------------------
 2012-03-05 18:00:00+01

Grassetto enfasi mio. Cos'è successo qua?
Ho scelto un offset di fuso orario arbitrario +3 per il letterale di input. 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 il fuso orario corrente impostato Vienna / Austria nel mio test, che ha un offset +1 durante l'inverno e +2 durante l'ora 2012-03-05 18:00:00+01 : 2012-03-05 18:00:00+01 , perché cade nel periodo 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' - tutto ha lo stesso identico valore interno.

AT TIME ZONE

Non appena riesci a capire questa logica, puoi fare tutto ciò che vuoi. Tutto ciò che manca ora è uno strumento per interpretare o rappresentare i valori letterali di un timestamp in base a un fuso orario specifico. È qui che entra in gioco il costrutto AT TIME ZONE . Ci sono due diversi casi d'uso. timestamptz viene convertito in timestamp e viceversa.

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

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

... che è equivalente a:

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

Per visualizzare lo stesso punto nel tempo come timestamp EST (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 valore di timestamp come (data) timestamp UTC restituendo il tipo timestamptz . Il secondo converte il timestamptz nel timestamp nel fuso orario specificato 'EST' - quale orologio nel fuso orario EST viene visualizzato in questo momento univoco.

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 una colonna timestamptz con lo stesso timestamp UTC 2012-03-05 17:00:00 . La 9a fila sembra funzionare nel mio fuso orario, ma è una trappola cattiva. Vedi sotto.

① Le righe 6 - 8 con il nome del fuso orario e l' abbreviazione del fuso orario per l'ora di Hawaii sono soggette all'ora legale (ora legale) e potrebbero essere diverse, anche se non al momento. Un nome di fuso orario come 'US/Hawaii' è a conoscenza delle regole dell'ora legale e di tutti i cambiamenti storici automaticamente, mentre un'abbreviazione come l' HST è solo un codice stupido per un offset fisso. Potrebbe essere necessario aggiungere un'abbreviazione diversa per l'ora legale / standard. Il nome interpreta correttamente qualsiasi timestamp nel fuso orario specificato. Un'abbreviazione è economica, ma deve essere quella giusta per il timestamp specificato:

L'ora legale non è tra le idee più brillanti che l'umanità abbia mai avuto.

② La riga 9, contrassegnata come arma caricata, funziona per me , ma solo per coincidenza. Se si esegue il cast esplicito di un timestamp [without time zone] , qualsiasi offset del fuso orario viene ignorato ! Viene utilizzato solo il timestamp nuda. Il valore viene quindi forzato automaticamente in timestamptz nell'esempio per corrispondere al tipo di colonna. Per questo passaggio, si assume l'impostazione del timezone della sessione corrente, che nel mio caso è lo stesso fuso orario +1 (Europa / Vienna). Ma probabilmente non nel tuo caso - il che si tradurrà in un valore diverso. In breve: non eseguire la conversione di timestamptz letterali in timestamp o si perde l'offset del fuso orario.

Le tue domande

L'utente memorizza un tempo, 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 stesso non viene mai memorizzato. Utilizzare uno dei metodi sopra riportati 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 fusi orari diversi.
Per il tempo globale assoluto:

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

Per tempo secondo l'orologio locale:

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

Non sei stanco delle informazioni di base, ancora? timestamp


Se vuoi operare in UTC di default:

In config/application.rb , aggiungere:

config.time_zone = 'UTC'

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

post.created_at.in_time_zone(current_user.timezone)

current_user.timezone dovrebbe essere un nome fuso orario valido, altrimenti si otterrà ArgumentError: Invalid Timezone orario non ArgumentError: Invalid Timezone , vedere l'elenco completo .







timezone