1. ホーム
  2. ruby-on-rails

[解決済み] RailsとPostgreSQLでタイムゾーンを完全に無視する

2022-04-22 03:45:13

質問

RailsとPostgresで日付と時間を扱っているのですが、このような問題に遭遇しています。

データベースはUTCです。

Railsアプリでユーザーが任意のタイムゾーンを設定しますが、これは時間を比較するためにユーザーのローカルタイムを取得するときにのみ使用されるものです。

ユーザーは、2012年3月17日午後7時という時間を保存します。タイムゾーンの変換やタイムゾーンを保存してほしいわけではありません。ただ、その日付と時刻を保存してほしいのです。そうすれば、ユーザーがタイムゾーンを変更しても、2012年3月17日午後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)

もっと簡単にタイムゾーンを無視する方法がありそうな気がするのですが。何かアイデアはありますか?

解決方法は?

Postgresには2つの異なるタイムスタンプデータタイプがあります。

timestamptz 好ましい は、文字通り日付/時刻の系列に属する型です。この型には typispreferred に設定されています。 pg_type というように、関連性を持たせることができます。

内部ストレージと エポック

内部的には、タイムスタンプは占有している 8バイト ディスクとRAMに保存されます。これは、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値。 (昔は  
* 秒単位のdouble値)

そして

/* UnixとPostgresの計算におけるユリウス暦の0日に相当するもの */  
#define UNIX_EPOCH_JDATE 2440588 /* == date2j(1970, 1, 1) */  
#define POSTGRES_EPOCH_JDATE 2451545 /* == date2j(2000, 1, 1) */  

マイクロ秒の分解能は、秒の小数点以下の桁数が最大6桁になるように変換されます。

timestamp

について timestamp タイムゾーンが明示的に指定されていません。ポストグレス 無視 入力リテラルに誤って追加されたタイムゾーン修飾子がある場合

時間をずらして表示することはありません。すべてが同じタイムゾーンで起きている場合、これは問題ありません。異なるタイムゾーンの場合は 意味 が変わりますが 価値 表示 はそのままです。

timestamptz

の取り扱いについて timestamptz が微妙に違う。 ここでマニュアルを引用します :

<ブロッククオート

について timestamp with time zone の場合、内部で保存されている値は 常にUTCで (万国標準時)

太字の強調は私です。その タイムゾーンそのものが保存されることはない . これは、UTC タイムスタンプを計算するための入力修飾子であり、 タイムゾーンオフセットを付加して保存したり、 表示用のローカルタイムを計算するための出力装飾子でもあります。オフセットを付加しない場合は timestamptz を入力すると、セッションの現在のタイムゾーン設定が想定されます。すべての計算は、UTCタイムスタンプ値で行われます。複数のタイムゾーンを扱う必要がある (かもしれない) 場合は、以下のようにします。 timestamptz . 言い換えれば もし、想定されるタイムゾーンに疑問や誤解がある場合は timestamptz . ほとんどのユースケースで適用されます。

psql や pgAdmin などのクライアント、あるいは libpq (Ruby の pg gem など) は、タイムスタンプと 現在のタイムゾーン または 要求された タイムゾーン(下記参照)。それは常に 同一時刻 表示形式が異なるだけです。あるいは マニュアルにあるように :

<ブロッククオート

タイムゾーンを考慮した日付と時刻は、すべて内部的にUTCで保存されます。それらは で指定されたゾーンのローカルタイムに変換されます。 TimeZone の設定パラメータを使用して、クライアントに表示されます。

psqlでの例。

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

ここで何が起こったのか?

任意のタイムゾーンオフセットを選択した +3 を入力リテラルに指定します。Postgresにとって、これはUTCタイムスタンプを入力するための多くの方法のうちの1つに過ぎません。 2012-03-05 17:00:00 . クエリの結果は 表示 現在のタイムゾーン設定に対して ウィーン/オーストリア 私のテストでは、オフセットを持つ +1 冬期と +2 は、夏時間(サマータイム)です。そのため 2012-03-05 18:00:00+01 DSTが始まるのが遅いため。

Postgresは入力リテラルをすぐに忘れてしまいます。覚えているのはデータ型に対応する値だけです。ちょうど10進数の場合のように。 numeric '003.4' または numeric '+3.4' - は、どちらも全く同じ内部値になります。

AT TIME ZONE

あとは、タイムスタンプリテラルを特定のタイムゾーンにしたがって解釈したり表現したりするツールがあればよいのです。そこで AT TIME ZONE の構成が入ります。2つの異なる使用例があります。 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' 2回 . 1つ目の解釈は timestamp の値を (与えられた) UTC タイムスタンプとして返します。 timestamptz . 2 番目の変換は timestamptztimestamp は、指定されたタイムゾーン '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)を返します。 同一 同じUTCタイムスタンプを持つtimestamptzカラムの行を持つ 2012-03-05 17:00:00 . 9行目は、私のタイムゾーンではたまたまうまくいくのですが、邪悪な罠です。下記をご覧ください。

6行目~8行目(タイムゾーンあり 名称 およびタイムゾーン 略語 は、DST(サマータイム)の影響を受けるため、異なる場合があります(現在はありません)。のようなタイムゾーン名は 'US/Hawaii' のような略語は、夏時間の規則とすべての歴史的なシフトを自動的に認識します。 HST は、固定オフセットのための単なるダムコードです。夏時間と標準時間には、別の略語を追加する必要があるかもしれません。その 名前 を正しく解釈します。 任意 のタイムスタンプを指定されたタイムゾーンで表示します。また 省略形 は安価ですが、与えられたタイムスタンプに対して正しいものである必要があります。

<サブ サマータイムは、人類が考え出した最も優れたアイデアのひとつではありません。

9行目、マーク ロードフットガン 作品 私の場合 しかし、それは偶然に過ぎません。もし、明示的にリテラルを timestamp [without time zone] , タイムゾーンのオフセットは無視されます ! 素のタイムスタンプのみが使用される。この値は自動的に timestamptz の例では、カラムの型に合わせるために このステップでは timezone の設定は、現在のセッションと同じタイムゾーンであると仮定します。 +1 は、私の場合(ヨーロッパ/ウィーン)。しかし、おそらくあなたの場合はそうではないでしょう - 違う値になります。要するに timestamptz リテラルを timestamp を使用すると、タイムゾーンのオフセットが失われます。

ご質問内容

<ブロッククオート

ユーザーは、2012年3月17日午後7時というように時間を保存します。私は、タイムゾーン の変換やタイムゾーンが保存されます。

タイムゾーンそのものが保存されることはありません。UTCタイムスタンプを入力するには、上記の方法のいずれかを使用してください。

<ブロッククオート

私は、ユーザーが指定したタイムゾーンを使用して、「前」または「後」のレコードを取得するだけです。 ユーザーのローカルタイムゾーンでの現在時刻より後。

異なるタイムゾーンのすべてのクライアントに1つのクエリを使用することができます。

グローバルな絶対時刻の場合。

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

ローカルクロックによる時刻の場合。

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

背景の情報にはまだ飽きてない? マニュアルにはもっとたくさん載っています。