Ignorando completamente os fusos horários no Rails e no PostgreSQL

164

Estou lidando com datas e horários no Rails e Postgres e me deparando com esse problema:

O banco de dados está no UTC.

O usuário define um fuso horário de escolha no aplicativo Rails, mas deve ser usado apenas ao obter o horário local dos usuários para comparar horários.

O usuário armazena um horário, digamos 17 de março de 2012, 19:00. Não quero que as conversões de fuso horário ou o fuso horário sejam armazenados. Eu só quero que data e hora sejam salvas. Dessa forma, se o usuário alterar seu fuso horário, ele ainda será exibido em 17 de março de 2012 às 19:00.

Uso apenas o fuso horário especificado pelo usuário para obter registros 'antes' ou 'depois' da hora atual no fuso horário local do usuário.

Atualmente, estou usando 'timestamp sem fuso horário', mas quando recupero os registros, os trilhos (?) Os convertem no fuso horário no aplicativo, o que não desejo.

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

Como os registros no banco de dados parecem sair como UTC, meu hack é levar o horário atual, remover o fuso horário com 'Date.strptime (str, "% m /% d /% Y")' e depois executar meu consulta com isso:

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

Parece que deve haver uma maneira mais fácil de simplesmente ignorar os fusos horários. Alguma ideia?

99 milhas
fonte

Respostas:

347

O tipo de dados timestampé o nome abreviado para timestamp without time zone.
A outra opção timestamptzé a abreviação de timestamp with time zone.

timestamptzé o tipo preferido na família de data / hora, literalmente. Foi typispreferreddefinido pg_type, o que pode ser relevante:

Armazenamento interno e época

Internamente, os carimbos de data / hora ocupam 8 bytes de armazenamento no disco e na RAM. É um valor inteiro representando a contagem de microssegundos da época do Postgres, 2000-01-01 00:00:00 UTC.

O Postgres também possui um conhecimento interno do tempo UNIX comumente usado, contando segundos da época UNIX, 1970-01-01 00:00:00 UTC, e usa isso em funções to_timestamp(double precision)ou EXTRACT(EPOCH FROM timestamptz).

O código fonte:

* Os carimbos de data e hora, bem como os campos h / m / s de intervalos, são armazenados como
* valores int64 com unidades de microssegundos. (Era uma vez eles estavam  
* valores duplos com unidades de segundos.)

E:

/ * Equivalentes à data juliana do dia 0 no cálculo de Unix e Postgres * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

A resolução de microssegundos é convertida em no máximo 6 dígitos fracionários por segundos.

timestamp

Um valor digitado como informa ao Postgres que nenhum fuso horário é fornecido explicitamente. O fuso horário atual é assumido. O Postgres ignora qualquer modificador de fuso horário adicionado por engano!timestamp [without time zone]

Nenhuma hora é alterada para exibição. Com o mesmo fuso horário, tudo está bem. Para um fuso horário diferente, o significado muda, mas o valor e a exibição permanecem os mesmos.

timestamptz

O manuseio timestamp with time zoneé sutilmente diferente. Cito o manual aqui :

Pois timestamp with time zone, o valor armazenado internamente está sempre em UTC (Universal Coordinated Time ...)

Negrito ênfase minha. O fuso horário em si nunca é armazenado . É um modificador de entrada usado para calcular o registro de data e hora UTC correspondente, que é armazenado - ou modificador de saída usado para calcular a hora local a ser exibida - com deslocamento de fuso horário anexado. Se você não anexar um deslocamento para timestamptzna entrada, a configuração de fuso horário atual da sessão será assumida. Todos os cálculos são feitos com valores de carimbo de data / hora UTC. Se você precisar (ou precisar) lidar com mais de um fuso horário, use timestamptz.

Clientes como psql ou pgAdmin ou qualquer aplicativo que esteja se comunicando via libpq (como Ruby com a pg gem) são apresentados com o carimbo de data / hora e o deslocamento para o fuso horário atual ou de acordo com o fuso horário solicitado (veja abaixo). É sempre o mesmo momento , apenas o formato de exibição varia. Ou, como o manual coloca :

Todas as datas e horas com reconhecimento de fuso horário são armazenadas internamente no UTC. Eles são convertidos para a hora local na zona especificada pelo parâmetro de configuração TimeZone antes de serem exibidos para o cliente.

Considere este exemplo simples (em psql):

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

Negrito ênfase minha. O que aconteceu aqui?
Eu escolhi um deslocamento de fuso horário arbitrário +3para o literal de entrada. Para o Postgres, esta é apenas uma das muitas maneiras de inserir o carimbo de data / hora UTC 2012-03-05 17:00:00. O resultado da consulta é exibido para a configuração atual do fuso horário Viena / Áustria em meu teste, que tem um deslocamento +1durante o inverno e +2durante o horário de verão:, 2012-03-05 18:00:00+01porque ocorre no horário de inverno.

O Postgres já esqueceu como esse valor foi inserido. Tudo o que lembra é o valor e o tipo de dados. Assim como com um número decimal. numeric '003.4', numeric '3.40'ou numeric '+3.4'- todos resultam exatamente no mesmo valor interno.

AT TIME ZONE

Assim que você entender essa lógica, poderá fazer o que quiser. Tudo o que está faltando agora é uma ferramenta para interpretar ou representar literais de registro de data e hora de acordo com um fuso horário específico. É aí que AT TIME ZONEentra a construção. Existem dois casos de uso diferentes. timestamptzé convertido timestampe vice-versa.

Para inserir o UTC timestamptz 2012-03-05 17:00:00+0:

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

... o que equivale a:

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

Para exibir o mesmo ponto no tempo que o EST timestamp(Horário padrão do leste):

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

Isso mesmo, AT TIME ZONE 'UTC' duas vezes . O primeiro interpreta o timestampvalor como (dado) carimbo de data / hora UTC retornando o tipo timestamptz. O segundo converte timestamptzpara o timestampno fuso horário fornecido 'EST' - o que um relógio no fuso horário EST exibe nesse momento único.

Exemplos

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

Retorna 8 (ou 9) linhas idênticas com colunas timestamptz com o mesmo registro de data e hora UTC 2012-03-05 17:00:00. Por acaso, a 9ª linha funciona no meu fuso horário, mas é uma armadilha do mal. Ver abaixo.

① As linhas 6 - 8 com o nome do fuso horário e a abreviação do fuso horário do Havaí estão sujeitas ao horário de verão (DST) e podem diferir, embora não atualmente. Um nome de fuso horário como 'US/Hawaii'conhece as regras de horário de verão e todos os históricos mudam automaticamente, enquanto uma abreviação como HSTé apenas um código idiota para um deslocamento fixo. Pode ser necessário anexar uma abreviação diferente para o horário de verão / padrão. O nome interpreta corretamente qualquer carimbo de data / hora no fuso horário especificado. Uma abreviação é barata, mas precisa ser a correta para o carimbo de data / hora especificado:

O horário de verão não está entre as idéias mais brilhantes que a humanidade já teve.

② A linha 9, marcada como espingarda carregada, funciona para mim , mas apenas por coincidência. Se você converter explicitamente um literal para timestamp [without time zone], qualquer deslocamento de fuso horário será ignorado ! Somente o registro de data e hora simples é usado. O valor é coagido automaticamente timestamptzno exemplo para corresponder ao tipo de coluna. Para esta etapa, timezoneé assumida a configuração da sessão atual, que passa a ser o mesmo fuso horário +1no meu caso (Europa / Viena). Mas provavelmente não no seu caso - o que resultará em um valor diferente. Em suma: Não molde timestamptzliterais para timestampou você perde o deslocamento de fuso horário.

Suas perguntas

O usuário armazena um horário, digamos 17 de março de 2012, 19:00. Não quero que as conversões de fuso horário ou o fuso horário sejam armazenados.

O fuso horário em si nunca é armazenado. Use um dos métodos acima para inserir um carimbo de data / hora UTC.

Uso apenas o fuso horário especificado pelo usuário para obter registros 'antes' ou 'depois' da hora atual no fuso horário local do usuário.

Você pode usar uma consulta para todos os clientes em diferentes fusos horários.
Para o tempo global absoluto:

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

Para o tempo de acordo com o relógio local:

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

Ainda não está cansado de informações básicas? Há mais no manual.

Erwin Brandstetter
fonte
2
Pequenos detalhes, mas acho que os carimbos de data e hora são armazenados internamente como o número de microssegundos desde 01-01-200 2000 - consulte a seção de data / hora do tipo de dados do manual. Minhas próprias inspeções da fonte parecem confirmar isso. Estranho usar uma origem diferente para a época!
harmic
2
@harmic Quanto a épocas diferentes ... Na verdade, não é tão estranho. Esta página da Wikipedia lista duas dúzias de épocas usadas por vários sistemas de computador. Embora a época do Unix seja comum, não é a única.
Basil Bourque 15/02
4
@ErwinBrandstetter Esta é uma ótima resposta, exceto por uma falha grave. Como comentário prejudicial, o Postgres não usa o tempo Unix. De acordo com o documento : (a) a época é 01-01-2001, em vez de 01-01-2009, e (b) Enquanto o tempo Unix possui uma resolução de segundos inteiros, o Postgres mantém frações de segundos. O número de dígitos fracionários depende da opção de tempo de compilação: 0 a 6 quando o armazenamento inteiro de oito bytes (padrão) é usado ou de 0 a 10 quando o armazenamento de ponto flutuante (obsoleto) é usado.
Basil Bourque
2
@BasilBourque: Estou ciente desse erro infeliz. Se você não se importa, você pode editá-lo. Vi algumas de suas respostas no passado e você é bom nisso. Mais uma edição minha forçaria isso ao wiki da comunidade - ao longo do tempo, dediquei muito esforço (e edições) para torná-lo claro e abrangente.
Erwin Brandstetter
2
CORREÇÃO: No meu comentário anterior, citei incorretamente a época do Postgres como 2001. Na verdade, é 2000 .
Basil Bourque 16/02
1

Se você deseja negociar no UTC por padrão:

Em config/application.rb, adicione:

config.time_zone = 'UTC'

Então, se você armazenar o nome atual do fuso horário do usuário, current_user.timezonepode dizer.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezonedeve ser um nome de fuso horário válido; caso contrário, você ArgumentError: Invalid Timezoneverá uma lista completa .

Dorian
fonte