Ігнорування часових поясів у Rails та PostgreSQL


164

Я маю справу з датами та часом у Rails і Postgres і стикаюся з цим питанням:

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

Користувач встановлює часовий пояс за вибором у додатку Rails, але його слід використовувати лише для отримання місцевим часом користувачів для порівняння часу.

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

Я використовую лише вказаний користувачем часовий пояс для отримання записів "до" або "після" поточного часу в локальному часовому поясі користувачів.

Зараз я використовую "часову позначку без часового поясу", але коли я отримую записи, рейли (?) Перетворюють їх у часовий пояс у додатку, чого я не хочу.

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

Оскільки записи в базі даних видаються як UTC, мій хак - це взяти поточний час, видалити часовий пояс із "Date.strptime (str,"% m /% d /% Y ")", а потім зробити мій запит із цим:

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

Здається, що повинен бути простіший спосіб просто ігнорувати часові пояси навколо. Будь-які ідеї?

Відповіді:


347

Тип даних timestamp- це коротке ім'я для timestamp without time zone.
Інший варіант timestamptzкороткий timestamp with time zone.

timestamptzє кращим типом у родині дат / час, буквально. Він typispreferredвстановив pg_type, що може бути актуальним:

Внутрішнє зберігання та епоха

Всередині часові позначки займають 8 байт пам'яті на диску та в оперативній пам'яті. Це ціле значення, що представляє кількість мікросекунд епохи Постгреса, 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

Значення, введене як повідомляє Postgres, що жоден часовий пояс не надається явно. Поточний часовий пояс передбачається. Postgres ігнорує будь-який модифікатор часового поясу, доданий помилково!timestamp [without time zone]

Години не зміщуються для показу. З однаковим налаштуванням часового поясу все добре. Для іншого часового поясу значення змінюється, але значення та відображення залишаються однаковими.

timestamptz

Поводження з ними timestamp with time zoneтонко відрізняється. Цитую посібник тут :

Бо timestamp with time zoneвнутрішньо збережене значення завжди в UTC (Універсальний координований час ...)

Сміливий акцент мій. Сам часовий пояс ніколи не зберігається . Це модифікатор входу, який використовується для обчислення часової позначки UTC, яка зберігається - або модифікатора виводу, що використовується для обчислення місцевого часу для відображення - з доданим зміщенням часового поясу. Якщо ви не додаєте зміщення для timestamptzвведення, передбачається поточне налаштування часового поясу сеансу. Всі обчислення проводяться зі значеннями часових позначок UTC. Якщо вам доведеться (або, можливо, доведеться) мати справу з більш ніж одним часовим поясом, використовуйте timestamptz.

Клієнти, такі як psql або pgAdmin або будь-яка програма, що спілкується через libpq (наприклад, Ruby з gg gg ), мають часову позначку плюс зміщення для поточного часового поясу або відповідно до запитуваного часового поясу (див. Нижче). Це завжди однаковий момент часу , змінюється лише формат відображення. Або, як зазначено в посібнику :

Усі дати та часові часові зони зберігаються всередині UTC. Вони перетворюються на місцевий час у зоні, визначеній параметром конфігурації TimeZone, перш ніж відображатися клієнту.

Розглянемо цей простий приклад (у psql):

db = # SELECT 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. Сорт 9-го ряду, можливо, працює у моєму часовому поясі, але є злою пасткою. Дивись нижче.

① Рядки 6 - 8 з назвою часового поясу та абревіатурою часового поясу для часу на Гаваях підпадають під дію DST (літній час) і можуть відрізнятися, хоча зараз не є. Назва часового поясу, як-от 'US/Hawaii', знає правила DST і всі історичні зміни зміщуються автоматично, тоді як абревіатура, як-от, 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, дивіться повний список .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.