Игнорирование часовых поясов вообще в Rails и PostgreSQL
Я имею дело с датами и временем в Rails и Postgres и сталкиваюсь с этой проблемой:
База данных находится в UTC.
Пользователь устанавливает часовой пояс по своему выбору в приложении Rails, но он используется только при получении локального времени пользователя для сравнения времени.
Пользователь хранит время, скажем, 17 марта 2012 года, 19:00. Я не хочу, чтобы преобразования часового пояса или часовой пояс были сохранены. Я просто хочу сохранить эту дату и время. Таким образом, если пользователь изменил свой часовой пояс, он все равно будет отображаться 17 марта 2012 года, 7 вечера.
Я использую указанный пользователем часовой пояс только для получения записей "до" или "после" текущего времени в местном часовом поясе пользователей.
В настоящее время я использую "метку времени без часового пояса", но когда я получаю записи, 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)
Кажется, должен быть более простой способ просто игнорировать часовые пояса вокруг. Есть идеи?
4 ответа
Тип данных 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. Эпоха 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), предоставляется метка времени со смещением для текущего часового пояса или в соответствии с запрошенным часовым поясом (см. Ниже). Это всегда один и тот же момент времени, меняется только формат отображения. Или, как сказано в руководстве:
Все даты и время с учетом часового пояса хранятся внутри UTC. Они преобразуются в местное время в зоне, указанной параметром конфигурации TimeZone, перед тем, как отобразиться клиенту.
Рассмотрим этот простой пример (в psql):
db = # ВЫБРАТЬ timestamptz '2012-03-05 20:00 +03 '; timestamptz ------------------------ 2012-03-05 18:00:00 +01
Жирный акцент мой. Что здесь случилось?
Я выбрал произвольное смещение часового пояса +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') -- ①
, (9, timestamp '2012-03-05 18:00:00+1') -- ② loaded footgun!
) t(id, ts);
Возвращает 8 (или 9) одинаковых строк со столбцами timestamptz, содержащих одну и ту же метку времени UTC 2012-03-05 17:00:00
, Девятый ряд вроде работает в моем часовом поясе, но это злая ловушка. Увидеть ниже.
① Строки 6 - 8 с названием часового пояса и сокращением часового пояса для времени на Гавайях зависят от летнего времени (летнее время) и могут отличаться, хотя и не в настоящее время. Название часового пояса, например 'US/Hawaii'
знает о правилах летнего времени и всех исторических сдвигах автоматически, в то время как сокращение как HST
это просто тупой код для фиксированного смещения. Возможно, вам придется добавить другую аббревиатуру для летнего / стандартного времени. Имя правильно интерпретирует любую метку времени в данном часовом поясе. Сокращение дешево, но должно быть правильным для данной отметки времени:
Летнее время не входит в число самых ярких идей, когда-либо выдвинутых человечеством.
9 Строка 9, помеченная как заряженное ружье, работает на меня, но только по стечению обстоятельств. Если вы явно приведете литерал к timestamp [without time zone]
любое смещение часового пояса игнорируется! Используется только голая метка времени. Затем значение автоматически приводится к timestamptz
в примере, чтобы соответствовать типу столбца. Для этого шага timezone
предполагается установка текущего сеанса, который совпадает с часовым поясом +1
в моем случае (Европа / Вена). Но, вероятно, не в вашем случае - что приведет к другому значению. Короче говоря: не бросайте timestamptz
литералы к timestamp
или вы потеряете смещение часового пояса.
Ваши вопросы
Пользователь хранит время, скажем, 17 марта 2012 года, 19:00. Я не хочу, чтобы преобразования часового пояса или часовой пояс были сохранены.
Сам часовой пояс никогда не сохраняется. Используйте один из методов выше, чтобы ввести метку времени UTC.
Я использую указанный пользователем часовой пояс только для получения записей "до" или "после" текущего времени в местном часовом поясе пользователей.
Вы можете использовать один запрос для всех клиентов в разных часовых поясах.
Для абсолютного глобального времени:
SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time
Для времени по местным часам:
SELECT * FROM tbl WHERE time_col > now()::time
Еще не надоела справочная информация? Есть больше в руководстве.
Не знаю, содержит ли ответ Эрвина решение проблемы (все же он содержит массу полезной информации), но у меня есть
более короткое решение:
(хотя бы короче для чтения)
.where("created_at > ?", (YOUR_DATE_IN_THE_TIMEZONE).iso8601)
Почему происходит весь беспорядок
Когда вы пытаетесь реализовать что-то вроде
.where("created_at > ?", YOUR_DATE_IN_THE_TIMEZONE)
Rails по-прежнему использует время сервера (скорее всего, UTC) для преобразования даты в метку времени (метка времени без формата часового пояса). Вот почему все твои танцы с
in_time_zone
и тд бесполезно.
Почему iso8601 работает
Когда ты звонишь
iso8601
ваша дата преобразуется в строку, которую Rails не может "затормозить" и должна передать Postgres как есть.
Не забудьте проголосовать!
Если вы хотите иметь дело в 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
смотрите полный список.
У меня была аналогичная головоломка плюс точность временной метки в моей среде Angular / Typescript / Node API / PostgreSQL, вот полный ответ и решение