在rails 3.2中存储带时区的时间戳

Ras*_*kov 9 ruby postgresql activerecord ruby-on-rails ruby-on-rails-3

我正在尝试使用其包含的时区将所有时间戳存储在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-adaptergem 连接.列类型是带时区的时间戳.

我已经activerecord-native_db_types_override通过在我的environment.rb中添加以下行来更改gem 的自然activerecord映射:

  NativeDbTypesOverride.configure({
    postgres: {
      datetime: { name: "timestamp with time zone" },
      timestamp: { name: "timestamp with time zone" }
    }
  })
Run Code Online (Sandbox Code Playgroud)

我的application.rb目前包含

  config.active_record.default_timezone = :utc
  config.time_zone = "Pacific Time (US & Canada)"
Run Code Online (Sandbox Code Playgroud)

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

小智 5

在与同样的问题撞上我的头后,我了解到事情的可悲真相:

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

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

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

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

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

  1. 将时区存储在字符串列中(粗略,我知道)。
  2. 与其覆盖 to_s,不如写一个 getter。

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

def local_explodes_at
  explodes_at.in_time_zone(self.time_zone)
end
Run Code Online (Sandbox Code Playgroud)

如果要自动存储时区,请覆盖您的 setter:

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

为了确保 t.zone 返回正确的时区,需要将 Time.zone 设置为正确的时区。您可以Time.zone使用环绕过滤器 (Railscast)轻松更改每个应用程序、用户或对象。有很多方法可以做到这一点,我喜欢 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
Run Code Online (Sandbox Code Playgroud)

这不包括 setter 方法,因为您确实不希望每个日期时间列都能够写入 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
Run Code Online (Sandbox Code Playgroud)

这是一个相关的有用答案:在 Rails 和 PostgreSQL 中完全忽略时区

希望这可以帮助!