ruby-on-rails 在Rails和PostgreSQL中完全忽略时区

gojuced7  于 2023-03-04  发布在  Ruby
关注(0)|答案(4)|浏览(159)

我在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)

看起来一定有一个更简单的方法可以忽略周围的时区。有什么想法吗?

pinkon5k

pinkon5k1#

Postgres有两种不同的时间戳数据类型:

timestamptz是日期/时间系列中的 * preferred * 类型,它在pg_type中设置了typispreferred,可以是相关的:

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

内部存储和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:

* Timestamps, as well as the h/m/s fields of intervals, are stored as
* int64 values with units of microseconds.  (Once upon a time they were  
* double values with units of seconds.)

以及:

/* Julian-date equivalents of Day 0 in Unix and Postgres reckoning */  
#define UNIX_EPOCH_JDATE        2440588 /* == date2j(1970, 1, 1) */  
#define POSTGRES_EPOCH_JDATE    2451545 /* == date2j(2000, 1, 1) */

微秒分辨率转换为秒的最大6个小数位数。

一米九一x

对于**timestamp**,没有显式提供时区。Postgres * 忽略 * 任何错误添加到输入文本中的时区修饰符!
显示时数没有变化。所有事情都发生在同一个时区,这很好。对于不同的时区,* 含义 * 会改变,但 * 值 * 和 * 显示 * 保持不变。

一米十一分

    • timestamptz**的处理有细微的不同。我在此引用手册:

对于timestamp with time zone,内部存储值为始终采用UTC(通用协调时间...)
我的。**时区本身不会被存储。它是一个输入修饰符,用于计算相应的UTC时间戳,它被存储-或和输出装饰器用于计算显示的本地时间-附加时区偏移量。如果你没有在输入上附加timestamptz的偏移量,假定会话的当前时区设置。所有计算均使用UTC时间戳值完成。如果您(可能)必须处理多个时区,请使用timestamptz。换句话说:如果对假设的时区有任何疑问或误解,请使用timestamptz。适用于大多数用例。
psql、pgAdmin等客户端或任何通过libpq通信的应用程序(如带有pg gem的Ruby)都会显示时间戳加上 * 当前时区 * 的偏移量,或者根据 * 请求的 * 时区(见下文)显示。它总是 * 相同的时间点 *,只是显示格式不同。或者,如手册所述:
所有时区识别的日期和时间都以UTC格式存储在内部。在显示给客户端之前,它们将被转换为TimeZone配置参数指定的时区的本地时间。
psql中的示例:

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

这里发生了什么事?
对于Postgres,这只是输入UTC时间戳2012-03-05 17:00:00的多种方法之一,在我的测试中,对于当前时区设置 * Vienna/Austria *,查询结果是 * displayed *,它在冬季的偏移量为+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时间timestamptz2012-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' * twice *。第一个函数将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)个 * 相同 * 的行,其中timestamptz列包含相同的UTC时间戳2012-03-05 17:00:00。第9行在我的时区中可以工作,但这是一个邪恶的陷阱。请参见下文。
①夏威夷时间时区***名称***和时区***缩写**的6 - 8行以DST为准(夏令时),并且可能不同,尽管当前不是。像'US/Hawaii'这样的时区名称自动知道DST规则和所有历史班次,而像HST这样的缩写只是固定偏移量的哑代码。您可能需要为夏令时/标准时间附加不同的缩写。 name * 正确地解释了给定时区的 * 任何 * 时间戳。一个 *缩写 * 是便宜的,但需要是给定时间戳的正确缩写:

②第9行,标记为 * loaded footgun * 对我 * 有效,但只是巧合。如果显式地将一个字面值强制转换为timestamp [without time zone],则忽略任何时区偏移!只使用裸时间戳。然后,在示例中,该值自动强制转换为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.

6ojccjat

6ojccjat2#

如果您想默认使用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

kulphzqa

kulphzqa3#

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

较短溶液:

  • (至少缩短一些便于阅读)*

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

别忘了投赞成票

bq3bfh9z

bq3bfh9z4#

在我的Angular/Typescript/Node API/PostgreSQL环境中,我也遇到过类似的难题,加上时间戳精度,这里是complete answer and solution

相关问题