首页 文章

在Rails和PostgreSQL中完全忽略时区

提问于
浏览
139

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

似乎必须有一种更简单的方法来忽略周围的时区 . 有任何想法吗?

2 回答

  • 0

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

    timestamptz 是日期/时间族中的首选类型,字面意思 . 它在pg_type中设置 typispreferred ,这可能是相关的:

    大纪元

    Internally ,时间戳存储为epoch的计数 . Postgres使用UTC中2000年第一天第一时刻的纪元,即2000-01-01T00:00:00Z . 八个octets用于存储计数 . 根据编译时选项,该数字是:

    • 8-byte integer (默认值),小数秒的0到6位数

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

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

    时间戳

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

    当您稍后显示 timestamp 时,您将获得从字面上输入的内容 . 使用相同的时区设置一切都很好 . 如果会话的时区设置发生更改, timestamp 的含义也会更改 - 值保持不变 .

    timestamptz

    处理 timestamp with time zone 略有不同 . I quote the manual here

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

    大胆强调我的 . time zone itself is never stored . 它是一个输入修饰符,用于计算相应的UTC时间戳,该时间戳存储 - 或用于计算显示的本地时间的输出修饰符 - 附加时区偏移量 . 如果未在输入上附加 timestamptz 的偏移量,则假定会话的当前时区设置 . 所有计算均使用UTC时间戳值完成 . 如果您必须(或可能必须)处理多个时区,请使用 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
    

    大胆强调我的 . What happened here?
    我为输入文字选择了一个任意时区偏移量 +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' - 都会产生完全相同的内部值 .

    时区

    一旦你掌握了这个逻辑,你就可以做任何你想做的事情 . 所有's missing now, is a tool to interpret or represent timestamp literals according to a specific time zone. That'是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 . 第二个在给定时区'EST'中将 timestamptz 转换为 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,时区 name 和时区 abbreviation 受夏令时(夏令时)的影响,可能会有所不同,但目前不同 . 像 'US/Hawaii' 这样的时区名称会自动识别DST规则和所有历史变化,而像 HST 这样的缩写只是固定偏移的哑码 . 您可能需要在夏季/标准时间附加不同的缩写 . 该名称正确解释给定时区的任何时间戳 . 缩写很便宜,但对于给定的时间戳,它必须是正确的缩写:

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

    ②第9行,标记为装载的步枪对我有用,但只是巧合 . 如果你明确地将文字转换为 timestamp [without time zone]any time zone offset is ignored!仅使用裸时间戳 . 然后,该值将自动强制转换为示例中的 timestamptz 以匹配列类型 . 对于此步骤,假定当前会话的 timezone 设置,恰好与我的情况(欧洲/维也纳)相同的时区 +1 . 但可能不是你的情况 - 这将导致不同的 Value . 简而言之:不要将 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.

  • 308

    如果您希望默认处理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 .

相关问题