Полное игнорирование часовых поясов в Rails и PostgreSQL

164

Я имею дело с датами и временем в 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)

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

99miles
источник

Ответы:

347

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

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

Внутренняя память и эпоха

Внутренне временные метки занимают 8 байт памяти на диске и в оперативной памяти. Это целочисленное значение, представляющее количество микросекунд из эпохи Postgres, 2000-01-01 00:00:00 UTC.

Postgres также обладает встроенными знаниями об общепринятых секундах подсчета времени в UNIX с эпохи UNIX, 1970-01-01 00:00:00 UTC, и использует их в функциях to_timestamp(double precision)или EXTRACT(EPOCH FROM timestamptz).

Исходный код:

* Метки времени, а также поля интервалов h / m / s хранятся как
* значения int64 с единицами микросекунд. (Когда-то они были  
* двойные значения с единицами секунд.)

И:

/ * Эквиваленты Юлианской даты Дня 0 в расчете Unix и Postgres * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

Разрешение микросекунды преобразуется максимум в 6 дробных цифр за секунды.

### timestamp Значение, набранное as, сообщает Postgres, что часовой пояс явно не указан. Текущий часовой пояс принят. Postgres игнорирует любой модификатор часового пояса, добавленный по ошибке!timestamp [without time zone]

Часы не сдвигаются для отображения. С такой же настройкой часового пояса все в порядке. Для другого часового пояса значение меняется, но значение и отображение остаются прежними.

### 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

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

Эрвин Брандштеттер
источник
2
Незначительные детали, но я думаю, что временные метки хранятся внутри как количество микросекунд с 2000-01-01 - см. Раздел «Тип данных даты / времени » в руководстве. Мои собственные проверки источника, кажется, подтверждают это. Странно использовать другое происхождение для эпохи!
вредный
2
@harmic Что касается другой эпохи ... На самом деле не так странно. На этой странице Википедии перечислены две дюжины эпох, используемых различными компьютерными системами. Хотя эпоха Unix распространена, она не единственная.
Василий Бурк
4
@ErwinBrandstetter Это отличный ответ, за исключением одного серьезного недостатка. Как заметил вред, Postgres не использует время Unix. Согласно документу : (a) эпоха 2001-01-01, а не Unix 1970-01-01, и (b) хотя время Unix имеет разрешение целых секунд, Postgres сохраняет доли секунд. Число дробных цифр зависит от параметра времени компиляции: от 0 до 6, если используется восьмибайтовое целочисленное хранилище (по умолчанию), или от 0 до 10, если используется хранилище с плавающей запятой (устарело).
Базилик Бурк
2
@BasilBourque: я знаю об этой печальной ошибке. Если вы не возражаете, вы можете редактировать его. Я видел некоторые из твоих ответов в прошлом, и ты хорош в этом. Еще одно редактирование от меня заставило бы это сделать вики сообщества - со временем я приложил немало усилий (и исправлений), чтобы сделать его понятным и всеобъемлющим.
Эрвин Брандштеттер,
2
ИСПРАВЛЕНИЕ: В своем предыдущем комментарии я неверно процитировал эпоху Постгреса как 2001 год. На самом деле это 2000 год .
Василий Бурк
1

Если вы хотите иметь дело в 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, см. полный список .

Дориан
источник