Игнорирование часовых поясов в Rails и PostgreSQL - PullRequest
149 голосов
/ 05 марта 2012

Я имею дело с датами и временем в Rails и Postgres и сталкиваюсь с этой проблемой:

База данных находится в UTC.

Пользователь устанавливает часовой пояс в приложении Rails, но он используется только при получении локального времени пользователя для сравнения времени.

Пользователь сохраняет время, скажем, 17 марта 2012 года, 19:00. Я не хочу, чтобы преобразования часового пояса или часовой пояс были сохранены. Я просто хочу сохранить эту дату и время. Таким образом, если пользователь изменил свой часовой пояс, он все равно будет отображаться 17 марта 2012 года, в 19:00.

Я использую указанный пользователем часовой пояс только для получения записей «до» или «после» текущего времени в местном часовом поясе пользователей.

В настоящее время я использую «метку времени без часового пояса», но когда я получаю записи, rails (?) Преобразует их в часовой пояс в приложении, что мне не нужно.

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

Поскольку записи в базе данных выглядят как UTC, мой хак - взять текущее время, удалить часовой пояс с помощью Date.strptime (str, "% m /% d /% Y") 'и затем сделайте мой запрос с этим:

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

Кажется, должен быть более простой способ просто игнорировать часовые пояса вокруг. Есть идеи?

Ответы [ 2 ]

325 голосов
/ 06 марта 2012

Тип данных timestamp - короткое имя для timestamp without time zone.
Другой вариант timestamptz - это сокращение от timestamp with time zone.

timestamptz - это буквально предпочтительный тип в семье даты / времени.typispreferred установлено в pg_type, что может иметь значение:

Эпоха

Внутренне , временные метки сохраняются как отсчет от эпохи .Postgres использует эпоху первого момента первого дня 2000 года в UTC, то есть 2000-01-01T00: 00: 00Z.Восемь октетов используются для хранения счетного числа.В зависимости от параметра времени компиляции это число может быть либо:

  • 8-байтовое целое число (по умолчанию), с 0 до 6 цифрами дробной секунды
  • Число с плавающей запятой (устарело), ​​с 0 до 10 цифр доли секунды, где точность быстро снижается для значений, удаленных от эпохи.
    В современных установках Postgres используется 8-байтовое целое число.

Обратите внимание, что Postgres не использует Unix time .Эпоха Postgres - это первый момент 2000-01-01, а не Unix 1970-01-01.В то время как время Unix имеет разрешение целых секунд, Postgres сохраняет доли секунд.

timestamp

Если вы определяете тип данных timestamp [without time zone], вы говорите Postgres: «Я не предоставляю часовой пояс явно, предположим, чтовместо этого текущий часовой пояс. Postgres сохраняет временную метку как есть - , игнорируя модификатор часового пояса, если вы добавите его!

Когда вы позже отобразите эту timestamp,Вы получаете обратно то, что ввели буквально. С той же настройкой часового пояса все в порядке. Если настройки часового пояса для сеанса изменяются, то значение timestamp - значение остается прежним.

timestamptz

Обработка timestamp with time zone немного отличается. Я цитирую руководство здесь :

Для timestamp with time zone внутренне сохраненное значение равно всегда в UTC (Всемирное координированное время ...)

Мой жирный шрифт. Сам часовой пояс никогда не бываетсохранено . Это модификатор ввода, используемый для вычисления соответствующей метки времени UTC, которая сохраняется - или и модификатор выводаОн раньше вычислял местное время для отображения с добавленным смещением часового пояса.Если вы не добавляете смещение для timestamptz на входе, предполагается текущая настройка часового пояса сеанса.Все вычисления выполняются со значениями меток времени UTC.Если вам нужно (или может потребоваться) иметь дело с несколькими часовыми поясами, используйте timestamptz.

Клиенты, такие как psql или pgAdmin, или любое приложение, связывающееся через libpq (например, Ruby сpg gem) представлены с отметкой времени плюс смещение для текущего часового пояса или в соответствии с запрошенным часовым поясом (см. ниже).Это всегда один и тот же момент времени , меняется только формат отображения.Или , как указано в руководстве :

Все даты и время с учетом часового пояса хранятся внутри UTC.Они преобразуются в местное время в зоне, указанной параметром конфигурации TimeZone , перед тем как отобразиться на клиенте.

Рассмотрим этот простой пример (в psql):

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

Смелый акцент мой. Что здесь произошло?
Я выбрал произвольное смещение часового пояса +3 для входного литерала.Для Postgres это только один из многих способов ввода метки времени UTC 2012-03-05 17:00:00.Результат запроса отображается для текущего значения часового пояса Вена / Австрия в моем тесте, который имеет смещение +1 зимой и +2 летом: 2012-03-05 18:00:00+01, потому что оно выпадает на зимнее время.

Postgres уже забыл, как это значение было введено.Все, что он помнит, это значение и тип данных.Так же, как с десятичным числом.numeric '003.4', numeric '3.40' или numeric '+3.4' - все приводят к одному и тому же внутреннему значению.

AT TIME ZONE

Как только вы овладеете этой логикой, вы сможете сделатьвсе, что вы хотите.Все, чего сейчас не хватает, - это инструмент для интерпретации или представления литералов меток времени в соответствии с конкретным часовым поясом.Вот тут и появляется конструкция AT TIME ZONE. Есть два разных варианта использования.timestamptz преобразуется в timestamp и наоборот.

Для ввода UTC timestamptz 2012-03-05 17:00:00+0:

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

..., что эквивалентно:

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

Для отображения того же момента времени, что и EST timestamp (восточное стандартное время):

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

Это верно, AT TIME ZONE 'UTC' дважды .Первая интерпретирует значение timestamp как (заданную) метку времени UTC, возвращающую тип timestamptz.Второй преобразует timestamptz в timestamp в заданном часовом поясе 'EST' - что часы в часовом поясе EST отображают в этот уникальный момент времени.

Примеры

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')        -- ①
    <strike>, (9, timestamp   '2012-03-05 18:00:00+1')</strike>  -- ② loaded footgun!
      ) t(id, ts);

Возвращает 8 (или 9) идентичных строк со столбцами timestamptz, содержащих одинаковую метку времени UTC 2012-03-05 17:00:00.Девятый ряд вроде работает в моем часовом поясе, но это злая ловушка.См. Ниже.

① Строки 6 - 8 с часовым поясом имя и часовым поясом сокращение для времени на Гавайях:подвержены летнему времени (летнее время) и могут отличаться, но не в настоящее время.Название часового пояса, например 'US/Hawaii', знает о правилах перехода на летнее время и всех исторических сдвигах автоматически, а сокращение, например HST, является просто тупым кодом для фиксированного смещения.Возможно, вам придется добавить другую аббревиатуру для летнего / стандартного времени. имя правильно интерпретирует любую метку времени в данном часовом поясе.Аббревиатура дешевая, но должна быть правильной для данной временной метки:

Переход на летнее время не входит в число самых ярких идей, когда-либо выдвинутых человечеством.

② Строка 9, отмеченная как заряженный ноготь у меня работает , но только по стечению обстоятельств.Если вы явно приводите литерал к timestamp [without time zone], , любое смещение часового пояса игнорируется !Используется только голая метка времени.Затем значение автоматически приводится к timestamptz в примере, чтобы соответствовать типу столбца.Для этого шага предполагается настройка timezone текущего сеанса, которая в моем случае совпадает с часовым поясом +1 (Европа / Вена).Но, вероятно, не в вашем случае - что приведет к другому значению.Вкратце: не приводите timestamptz литералов к timestamp, иначе вы потеряете смещение часового пояса.

Ваши вопросы

Пользователь сохраняет время, скажем, 17 марта 2012 г.7 вечера.Я не хочу, чтобы преобразования часового пояса или часовой пояс были сохранены.

Сам часовой пояс никогда не сохраняется.Используйте один из перечисленных выше способов для ввода метки времени UTC.

Я использую указанный пользователем часовой пояс только для получения записей «до» или «после» текущего времени в местном часовом поясе пользователей.

Вы можете использовать один запрос для всех клиентов в разных часовых поясах.
Для абсолютного глобального времени:

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

Для времени по местным часам:

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

Еще не надоела справочная информация? В руководстве есть больше.

1 голос
/ 31 января 2017

Если вы хотите иметь дело в UTC по умолчанию:

В config/application.rb, добавьте:

config.time_zone = 'UTC'

Затем, если вы сохраняете текущий часовой пояс, имя пользователя будет current_user.timezoneМожно сказать.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezone должно быть допустимым именем часового пояса, в противном случае вы получите ArgumentError: Invalid Timezone, см. полный список .

...