【问题标题】:Store timestamps with timezone in rails 3.2在 Rails 3.2 中存储带有时区的时间戳
【发布时间】:2014-01-07 10:35:24
【问题描述】:

我正在尝试将所有时间戳存储在包含时区的 rails 应用程序中。我对 ActiveRecord 将它们转换为 utc 没问题,但是我有多个应用程序访问同一个数据库,其中一些是根据时区要求实现的。所以我想要做的是让 activerecord 像往常一样转换我的时间戳,然后将它们写入数据库,并将字符串“America/Los_Angeles”或任何适当的时区附加到时间戳。我目前在 jruby 1.7.8 上运行 rails 3.2.13,它实现了 ruby​​ 1.9.3 api。我的数据库是 postgres 9.2.4,与 activerecord-jdbcpostgresql-adapter gem 连接。列类型为带时区的时间戳。

我已经使用 activerecord-native_db_types_override gem 更改了自然的 activerecord 映射,方法是将以下几行添加到我的 environment.rb 中:

  NativeDbTypesOverride.configure({
    postgres: {
      datetime: { name: "timestamp with time zone" },
      timestamp: { name: "timestamp with time zone" }
    }
  })

我的 application.rb 当前包含

  config.active_record.default_timezone = :utc
  config.time_zone = "Pacific Time (US & Canada)"

我怀疑我可以重写 ActiveSupport::TimeWithZone.to_s 并更改它的 :db 格式以输出正确的字符串,但我还不能完成这项工作。非常感谢任何帮助。

【问题讨论】:

  • 为什么不将它们处理/存储为“带时区的时间戳”(utc)并设置 pg-client 时区,以便它返回您想要的格式?
  • 你可以设置 postgres 来做这个吗?如果是这样的话,这将适用于我的目的,但它们需要与附加的 UTC 时区一起存储。
  • 据我所知,您无法在进入数据库的过程中自动转换。我认为这无论如何都不可行,因为它还会自动转换连接到数据库的其他应用程序的时间戳,这可能是他们期望的,也可能是不期望的。

标签: ruby-on-rails ruby ruby-on-rails-3 postgresql activerecord


【解决方案1】:

在遇到同样的问题后,我了解到事情的可悲真相:

Postgres 不支持在其任何日期/时间类型中存储时区

因此,您根本无法将单个时刻及其时区存储在一个列中。在我提出替代解决方案之前,让我用Postgres docs 支持一下:

所有可识别时区的日期和时间都以 UTC 格式在内部存储。在显示给客户端之前,它们会转换为 TimeZone 配置参数指定的区域中的本地时间。

因此,您没有什么好方法可以简单地将时区“附加”到时间戳。但这并不是那么可怕,我保证!这只是意味着您需要另一列。

我提出的(相当简单的)解决方案:

  1. 将时区存储在字符串列中(我知道是粗略的)。
  2. 不要覆盖 to_s,只需写一个 getter。

假设您在 explodes_at 列上需要这个:

def local_explodes_at
  explodes_at.in_time_zone(self.time_zone)
end

如果您想自动存储时区,请覆盖您的设置器:

def explodes_at=(t)
  self.explodes_at = t
  self.time_zone = t.zone #Assumes that the time stamp has the correct offset already
end

为了确保 t.zone 返回正确的时区,需要将 Time.zone 设置为正确的时区。您可以使用an around filter (Railscast) 为每个应用程序、用户或对象轻松更改Time.zone。有很多方法可以做到这一点,我很喜欢 Ryan Bates 的方法,因此请以对您的应用程序有意义的方式实现它。

如果你想变得花哨,并且需要在多个列上使用这个 getter,你可以遍历所有列并为每个日期时间定义一个方法:

YourModel.columns.each do |c|
  if c.type == :datetime
    define_method "local_#{c.name}" do
      self.send(c.name).in_time_zone(self.time_zone)                                                                                                                           
    end
  end
end

YourModel.first.local_created_at #=> Works.
YourModel.first.local_updated_at #=> This, too.
YourModel.first.local_explodes_at #=> Ooo la la

这不包括 setter 方法,因为您确实不希望每个 datetime 列都能够写入 self.time_zone。您必须决定在哪里使用它。如果您想真正变得花哨,您可以通过在模块中定义它并将其导入每个模型来在所有模型中实现这一点。

module AwesomeDateTimeReader
  self.columns.each do |c|
    if c.type == :datetime
      define_method "local_#{c.name}" do
        self.send(c.name).in_time_zone(self.time_zone)                                                                                                                           
      end
    end
  end
end

class YourModel < ActiveRecord::Base
  include AwesomeDateTimeReader
  ...
end

这是一个相关的有用答案:Ignoring timezones altogether in Rails and PostgreSQL

希望这会有所帮助!

【讨论】:

  • 非常感谢!这加上相关的答案帮助了很多。实际上,一旦我能够证明 Postgres 不存储时区数据,我实际上找到了另一种解决方法。我将以稍微不同的形式使用您的模块解决方案,以确保显示端所有时间戳的正确格式。
【解决方案2】:

我可以建议将它们保存在 iso8601 中

这将允许您:

  • 也可以选择将它们存储为 UTC 与时区偏移一样
  • 符合国际标准

  • 在两种情况下使用相同的存储格式,偏移量和 没有。

因此,其中一个 db 列可以使用 UTC 格式的偏移量(通常)。

从 Ruby 方面来说,它很简单

Time.now.iso8601
Time.now.utc.iso8601

ActiveRecord 应该与转换无缝协作。

此外,大多数 API 都使用这种格式 (google),因此最适合跨应用兼容性。

postgresql 的 to_char() 应该为您提供正确的格式,以防默认设置出现任何问题。

【讨论】:

    【解决方案3】:

    按照您的建议,一种方法是覆盖 ActiveSupport::TimeWithZone.to_s

    你可以试试这样的:

    def to_s(format = :default)
      if format == :db
        time_with_timezone_format
      elsif formatter = ::Time::DATE_FORMATS[format]
        formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
      else
        time_with_timezone_format
      end
    end
    
    private
    
    def time_with_timezone_format
      "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format
    end
    

    我尚未对此进行测试,但查看 Postgres' docs on Time Stamps,这看起来有效,并且会给出类似的时间:"2013-12-26 10:41:50 +0000"

    据我所知,问题在于您仍然无法返回正确的时区:

    对于带时区的时间戳,内部存储的值始终采用 UTC(通用协调时间,传统上称为格林威治标准时间,GMT)。指定了明确时区的输入值将使用该时区的适当偏移量转换为 UTC。

    这正是原来的ActiveSupport::TimeWithZone.to_s 已经在做的事情。

    因此,获得正确时区的最佳方法可能是将明确的时区值设置为数据库中的新列。

    这意味着您将能够保留 Postgres 和 Rails 的本地日期功能,同时还能够在必要时以正确的时区显示时间。

    您可以使用这个新列,然后使用 Ruby 的 Time::getlocal 或 Rails 的 ActiveSupport::TimeWithZone.in_time_zone 显示正确的区域。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-05-06
      • 2016-11-11
      • 1970-01-01
      • 2017-11-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多