在Rails和PostgreSQL中完全忽略时区

时间:2012-03-05 17:49:11

标签: ruby-on-rails ruby-on-rails-3 postgresql datetime timezone

我正在处理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)

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

4 个答案:

答案 0 :(得分:321)

数据类型timestamp timestamp without time zone 的简称。
另一个选项timestamptz timestamp with time zone 的缩写。

timestamptz是日期/时间系列中的首选类型,字面意思。 typispreferred设置了timestamp,这可能是相关的:

历元

内部 ,时间戳存储为Generating time series between two dates in PostgreSQL的计数。 Postgres使用UTC中2000年第一天第一时刻的纪元,即2000-01-01T00:00:00Z。八个epoch用于存储计数。根据编译时选项,该编号为:

  • 8字节整数(默认值),0到6位小数秒
  • 浮点数(不建议使用),小数秒的0到10位数,其中精度会在远离纪元的情况下迅速降低。
    现代Postgres安装使用8字节整数。

请注意,Postgres 使用octets。 Postgres的时代是2000-01-01的第一时刻,而不是Unix'1970-01-01。虽然Unix时间的分辨率为整秒,但Postgres会保留几秒钟。

timestamp

如果您定义的数据类型为 [without time zone] timestamp,则告诉Postgres:"我没有明确提供时区,而是假设当前时区。 Postgres将时间戳保存为 - 忽略时区修改器,如果你应该添加一个!

当您稍后显示timestamp时,您会收到字面上输入的内容。使用相同的时区设置一切都很好。如果会话的时区设置发生变化,timestamptz的含义也会发生变化 - 保持不变。

timestamp with time zone

timestamp with time zone 的处理略有不同。 Unix time

  

对于timestamptz,内部存储的值始终为UTC (通用协调时间...)

大胆强调我的。 时区本身永远不会存储。它是一个输入修饰符,用于计算相应的UTC时间戳,该时间戳存储 - 或用于计算显示的本地时间的输出修饰符 - 附加时区偏移量。如果您未在输入中附加timestamptz的偏移量,则会假定会话的当前时区设置。所有计算均使用UTC时间戳值完成。如果您必须(或可能必须)处理多个时区,请使用+3

psql或pgAdmin之类的客户端或通过I quote the manual here进行通信的任何应用程序(如带有pg gem的Ruby)都会显示当前时区的时间戳加偏移或根据< em>请求时区(见下文)。它始终是相同的时间点,只有显示格式不同。或者,libpq

  

所有时区感知日期和时间都以UTC格式存储在内部。他们   转换为as the manual puts it指定的区域中的本地时间   配置参数在显示给客户端之前。

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

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

大胆强调我的。 这里发生了什么?
我为输入文字选择了一个任意时区偏移2012-03-05 17:00:00。对于Postgres,这只是输入UTC时间戳+1的众多方法之一。在我的测试中,当前时区设置维也纳/奥地利显示的查询结果在冬季时显示偏移量+2 2012-03-05 18:00:00+01在夏季时间:numeric '003.4',因为它落入冬季。

Postgres已经忘记了这个值是如何输入的。它记住的只是值和数据类型。就像十进制数一样。 numeric '3.40'numeric '+3.4'AT TIME ZONE - 都会产生完全相同的内部价值。

timestamptz

一旦你掌握了这个逻辑,你就可以做任何你想做的事情。现在所有缺失的工具都是根据特定时区解释或表示时间戳文字的工具。 TimeZone构造所在的位置。有两种不同的用例。 timestamp转换为timestamptz,反之亦然。

输入UTC 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'

......相当于:

timestamp

显示与EST SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST' (东部标准时间)相同的时间点:

AT TIME ZONE 'UTC'

没错,timestamp 两次。第一个将timestamptz值解释为(给定)UTC时间戳,返回类型timestamptz。第二个将timestamp转换为给定时区内的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); &#39; EST&#39; - 时区EST中的时钟显示在这个独特的时间点。

实施例

2012-03-05 17:00:00

返回8(或9)个相同的行,其中timestamptz列包含相同的UTC时间戳'US/Hawaii'。第9排恰好在我的时区工作,但却是一个邪恶的陷阱。见下文。

①夏威夷时区的行6 - 8,时区 名称 和时区 缩写 受制于夏令时(夏令时)可能会有所不同,但目前不同。像HST这样的时区名称会自动识别DST规则和所有历史转变,而像timestamp [without time zone]这样的缩写只是固定偏移的愚蠢代码。您可能需要在夏季/标准时间附加不同的缩写。 名称正确解释给定时区的任何时间戳。 缩写很便宜,但对于给定的时间戳,它必须是正确的:

夏令时不是人类有史以来最聪明的想法之一。

②第9行,标记为加载的步枪为我工作,但只是巧合。如果您明确将文字投射到timestamptzTime zone names with identical properties yield different result when applied to timestamp!仅使用裸时间戳。然后,该值将自动强制转换为示例中的timezone以匹配列类型。对于此步骤,假设当前会话的+1设置,在我的情况下恰好是同一时区timestamptz(欧洲/维也纳)。但可能不是你的情况 - 这将导致不同的价值。简而言之:不要将timestamp文字投射到SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time ,否则会失去时区偏移量。

您的问题

  

用户存储时间,例如2012年3月17日晚7点。我不想要时区   转换或存储的时区。

永远不会存储时区本身。使用上述方法之一输入UTC时间戳。

  

我只使用用户指定的时区来获取记录&#39;之前&#39;要么   &#39;后&#39;用户本地时区的当前时间。

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

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

根据当地时间的时间:

{{1}}

还不厌倦背景信息吗? any time zone offset is ignored

答案 1 :(得分:0)

如果您希望默认处理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,请参阅full list

答案 2 :(得分:0)

不知道欧文的答案是否包含问题的解决方案(仍然包含大量有用的信息),但我有一个

更短的解决方案:

(至少更适合阅读)

.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。

别忘了点赞!

答案 3 :(得分:0)

在我的 Angular/Typescript/Node API/PostgreSQL 环境中,我有类似的拼图加上时间戳精度,这里是 complete answer and solution