在 Rails 和 PostgreSQL 中完全忽略时区
Posted
技术标签:
【中文标题】在 Rails 和 PostgreSQL 中完全忽略时区【英文标题】:Ignoring time zones altogether in Rails and PostgreSQL 【发布时间】:2012-03-23 04:55:07 【问题描述】:我在 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,所以我的 hack 是采用当前时间,使用 'Date.strptime(str, "%m/%d/%Y")' 删除时区和然后用那个做我的查询:
.where("time >= ?", date_start)
似乎必须有一种更简单的方法来忽略周围的时区。有什么想法吗?
【问题讨论】:
【参考方案1】:Postgres 有两种不同的时间戳数据类型:
timestamp with time zone
,简称:timestamptz
timestamp without time zone
,简称:timestamp
timestamptz
是日期/时间系列中的首选类型,字面意思。它在pg_type
中设置了typispreferred
,这可能是相关的:
内部存储和epoch
在内部,时间戳在磁盘和 RAM 中占用 8 字节 的存储空间。它是一个整数值,表示从 Postgres 纪元 2000-01-01 00:00:00 UTC 算起的微秒数。
Postgres 还内置了常用的 UNIX time 从 UNIX 纪元 1970-01-01 00:00:00 UTC 开始计数的知识,并在函数 to_timestamp(double precision)
或 EXTRACT(EPOCH FROM timestamptz)
中使用它。
The source code:
* 时间戳以及间隔的 h/m/s 字段存储为 * int64 值,以微秒为单位。 (从前他们是 * 以秒为单位的双精度值。)还有:
/* Unix 和 Postgres 计算中第 0 天的儒略日期等价物 */ #define UNIX_EPOCH_JDATE 2440588 /* == date2j(1970, 1, 1) */ #define POSTGRES_EPOCH_JDATE 2451545 /* == date2j(2000, 1, 1) */微秒分辨率转换为最多 6 个小数位的秒数。
timestamp
timestamp
没有明确提供时区。 Postgres 忽略错误添加到输入文字的任何时区修饰符!
显示时不会移动时间。一切都发生在同一时区,这很好。对于不同的时区,含义会发生变化,但值和显示保持不变。
timestamptz
timestamptz
的处理方式略有不同。 I quote the manual here:
对于
timestamp with time zone
,内部存储的值始终采用UTC(世界协调时间...)
我的大胆强调。 时区本身永远不会被存储。它是一个输入修饰符,用于计算相应的 UTC 时间戳,该时间戳被存储 - 或输出装饰器用于计算本地时间以进行显示 - 附加时区偏移量。如果您没有在输入时为timestamptz
附加偏移量,则假定会话的当前时区设置。所有计算均使用 UTC 时间戳值完成。如果您(可能)必须处理多个时区,请使用timestamptz
。换句话说:如果对假定的时区有任何疑问或误解,请使用timestamptz
。适用于大多数用例。
像 psql 或 pgAdmin 之类的客户端或通过 libpq 进行通信的任何应用程序(例如带有 pg gem 的 Ruby)会显示时间戳加上当前时区的偏移量 或根据 请求的时区(见下文)。它总是同一时间点,只是显示格式不同。或者,as the manual puts it:
所有可识别时区的日期和时间都以 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
的众多方法之一。在我的测试中,显示当前时区设置 Vienna/Austria 的查询结果,在冬季有偏移 +1
,在夏季有 +2
(“夏令时”,DST)。所以2012-03-05 18:00:00+01
因为 DST 只是稍后才开始。
Postgres 会立即忘记输入文字。它只记住数据类型的值。就像十进制数一样。 numeric '003.4'
或 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
转换为给定时区“EST”中的timestamp
- 此时挂钟在 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 时间戳 2012-03-05 17:00:00
的 timestamptz 列。第 9 行恰好在我的时区工作,但这是一个邪恶的陷阱。见下文。
① 第 6 - 8 行的时区 name 和时区 缩写 为夏威夷时间DST(夏令时),可能会有所不同,但目前没有。像 'US/Hawaii'
这样的时区名称会自动识别 DST 规则和所有历史变化,而像 HST
这样的缩写只是固定偏移量的愚蠢代码。您可能需要为夏季/标准时间附加不同的缩写。 name 正确解释了给定时区的 any 时间戳。 缩写很便宜,但必须是给定时间戳的正确缩写:
夏令时并不是人类有史以来最聪明的想法之一。
② 第 9 行,标记为 loaded footgun为我工作,但这只是巧合。如果您将文字显式转换为 timestamp [without time zone]
, any time zone offset is ignored!仅使用裸时间戳。然后在示例中将值自动强制转换为 timestamptz
以匹配列类型。对于这一步,假设当前会话的timezone
设置,在我的情况下恰好是同一时区+1
(欧洲/维也纳)。但可能不是您的情况 - 这将导致不同的值。简而言之:不要将 timestamptz
文字转换为 timestamp
否则您会丢失时区偏移量。
您的问题
用户存储一个时间,例如 2012 年 3 月 17 日晚上 7 点。我不想要时区 转换或要存储的时区。
时区本身永远不会被存储。使用上述方法之一输入 UTC 时间戳。
我只使用用户指定的时区来获取“之前”的记录或 '在'用户本地时区的当前时间之后。
您可以对不同时区的所有客户使用一个查询。 对于绝对全球时间:
SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time
时间以当地时钟为准:
SELECT * FROM tbl WHERE time_col > now()::time
还没有厌倦背景信息? There is more in the manual.
【讨论】:
次要细节,但我认为时间戳在内部存储为自 2000-01-01 以来的微秒数 - 请参阅手册的 date/time datatype 部分。我自己对来源的检查似乎证实了这一点。奇怪的是使用不同的起源作为时代! @harmic 至于不同的时代……其实并不奇怪。这个Wikipedia page 列出了各种计算机系统使用的两打 epoch。虽然Unix epoch 很常见,但它并不是唯一的。 @ErwinBrandstetter 这是一个很好的答案,除了一个严重的缺陷。正如harmic 评论的那样,Postgres 不使用 Unix 时间。根据the doc:(a) 纪元是 2001-01-01 而不是 Unix 的 1970-01-01,并且 (b) 虽然 Unix 时间的分辨率是整秒,但 Postgres 只保留几分之一秒。小数位数取决于编译时选项:使用 8 字节整数存储(默认)时为 0 到 6,使用浮点存储(不推荐使用)时为 0 到 10。 更正: 在我之前的评论中,我错误地将 Postgres 时代称为 2001。实际上它是 2000。 当时间戳列是表p
的列之一时,有没有办法让SELECT p.*
类似查询的AT TIME ZONE
语句。 ***.com/questions/39211953/…【参考方案2】:
如果你想默认以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。
【讨论】:
【参考方案3】:不知道欧文的答案是否包含问题的解决方案(仍然包含大量有用的信息),但我有一个
更短的解决方案:
(至少读起来更短)
.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。
别忘了点赞!
【讨论】:
【参考方案4】:在我的 Angular/Typescript/Node API/PostgreSQL 环境中,我有类似的谜题和时间戳精度,这里是 complete answer and solution
【讨论】:
以上是关于在 Rails 和 PostgreSQL 中完全忽略时区的主要内容,如果未能解决你的问题,请参考以下文章
升级到 OSX 10.7 Lion 后修复 Postgresql
使用 Rails + Postgresql + Heroku 构建数据库