在Rails和PostgreSQL中完全忽略时区

Ignoring time zones altogether in Rails and PostgreSQL

我正在处理Rails和Postgres中的日期和时间,并遇到了这个问题:

数据库采用UTC格式。

用户在Rails应用程序中设置了一个可选时区,但它仅在获取用户本地时间进行时间比较时使用。

用户存储时间,比如2012年3月17日下午7点。我不希望存储时区转换或时区。我只想节省那个日期和时间。这样,如果用户更改了时区,它仍然会显示2012年3月17日下午7点。

我只使用用户指定的时区在用户本地时区中获取"早于"或"晚于"当前时间的记录。

我目前正在使用"没有时区的时间戳",但是当我检索记录时,rails(?)将它们转换为应用程序中的时区,这是我不想要的。

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

因为数据库中的记录似乎是以UTC的形式出现的,所以我的方法是利用当前时间,删除带有"date.strptime(str,"%m/%d/%y")的时区,然后执行查询:

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

似乎必须有一种简单的方法来忽略周围的时区。有什么想法吗?


数据类型timestamptimestamp without time zone的简称。另一个选项timestamptztimestamp with time zone的缩写。好的。

从字面上看,timestamptz是日期/时间系列中的首选类型。它在pg_type中设置了typispreferred,可以是相关的:好的。

  • 在PostgreSQL中生成两个日期之间的时间序列

时代

在内部,时间戳存储为来自某个纪元的计数。Postgres使用的是2000年第一天的第一个时刻,即2000-01-01T00:00:00Z。八位字节用于存储计数数字。根据编译时选项的不同,该数字可以是:好的。

  • 8字节整数(默认值),0到6位小数秒
  • 一个浮点数字(已弃用),0到10位小数秒,其中精度会因远离epoch的值而迅速降低。现代Postgres安装使用8字节整数。

请注意,Postgres不使用Unix时间。Postgres的时代是2000-01-01的第一个时刻,而不是Unix的1970-01-01。虽然Unix时间的分辨率为整秒,但Postgres保留了几秒的分数。好的。timestamp

如果您定义了数据类型timestamp[without time zone],您将告诉postgres:"我没有明确提供时区,请假定为当前时区。Postgres按原样保存时间戳-如果应该添加时区修改器,则忽略它!好的。

当您稍后显示timestamp时,您可以从字面上恢复输入的内容。在相同的时区设置下,一切都很好。如果会话的时区设置发生变化,那么EDOCX1的含义也会发生变化(0)--该值保持不变。好的。timestamptz

timestamp with time zone的处理方式略有不同。我在这里引用手册:好的。

For timestamp with time zone, the internally stored value is always in UTC (Universal Coordinated Time ...)

Ok.

大胆强调我的。时区本身从不存储。它是一个输入修饰符,用于根据存储的UTC时间戳计算,或者是用于计算要显示的本地时间的输出修饰符,带有附加的时区偏移量。如果在输入时不附加timestamptz的偏移量,则假定会话的当前时区设置。所有计算都是用UTC时间戳值完成的。如果必须(或可能必须)处理多个时区,请使用timestamptz。好的。

像psql或pgadmin这样的客户机或任何通过libpq通信的应用程序(比如ruby和pg gem)都会显示当前时区的时间戳加偏移量,或者根据请求的时区(见下文)。它总是在同一时间点,只有显示格式不同。或者,正如手册所说:好的。

All timezone-aware dates and times are stored internally in UTC. They
are converted to local time in the zone specified by the TimeZone
configuration parameter before being displayed to the client.

Ok.

考虑这个简单的例子(在psql中):好的。

1
2
3
4
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,在夏季有一个偏移量+22012-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 timestamptz2012-03-05 17:00:00+0:好的。

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

…相当于:好的。

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

显示与东部标准时间(东部标准时间)相同的时间点:好的。

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

是的,两次。第一个将timestamp值解释为(给定的)返回类型timestamptz的UTC时间戳。第二个将给定时区"est"中的timestamptz转换为timestamp,即时区"est"中的时钟在这个唯一的时间点上显示的内容。好的。实例

1
2
3
4
5
6
7
8
9
10
11
12
13
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)个相同的行,其中时间戳列包含相同的UTC时间戳2012-03-05 17:00:00。第九排正好在我的时区工作,但却是一个邪恶的陷阱。见下文。好的。

①带有时区名称和夏威夷时间时区缩写的第6-8行以DST(夏令时)为准,可能有所不同,但目前没有。像'US/Hawaii'这样的时区名称可以自动知道DST规则和所有历史移位,而像HST这样的缩写只是固定偏移量的一个哑代码。您可能需要为夏季/标准时间添加不同的缩写。名称正确地解释了给定时区的任何时间戳。缩写很便宜,但必须是给定时间戳的正确缩写:好的。

  • 具有相同属性的时区名称在应用于时间戳时产生不同的结果

夏令时不是人类提出的最聪明的想法之一。好的。

②第9排,标记为"负重"的步兵为我工作,但只是巧合。如果您显式地将文本强制转换为timestamp [without time zone],则忽略任何时区偏移!只使用裸时间戳。然后,该值在示例中自动强制为timestamptz,以匹配列类型。在此步骤中,假设当前会议的timezone设置,在我的情况下(欧洲/维也纳),正好是同一时区+1。但在您的情况下可能不会-这将导致不同的值。简而言之:不要将timestamptz字面值强制转换为timestamp,否则会丢失时区偏移。好的。你的问题

User stores a time, say March 17, 2012, 7pm. I don't want timezone
conversions or the timezone to be stored.

Ok.

时区本身从不存储。使用上面的方法之一输入UTC时间戳。好的。

I only use the users specified time zone to get records 'before' or
'after' the current time in the users local time zone.

Ok.

您可以对不同时区的所有客户机使用一个查询。对于绝对全局时间:好的。

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

对于本地时钟的时间:好的。

1
SELECT * FROM tbl WHERE time_col > now()::TIME

还不厌倦背景信息吗?手册中还有更多内容。好的。好啊。


如果您希望默认使用UTC进行交易:

config/application.rb中,增加:

1
config.time_zone = 'UTC'

然后,如果您存储的当前用户时区名称是current_user.timezone,您可以说。

1
post.created_at.in_time_zone(CURRENT_USER.timezone)

current_user.timezone应该是一个有效的时区名称,否则您将得到ArgumentError: Invalid Timezone,请参阅完整的列表。