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

时间:2013-12-19 01:26:19

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

我正在尝试使用其包含的时区将所有时间戳存储在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格式输出正确的字符串,但我还没能完成那项工作。非常感谢任何帮助。

3 个答案:

答案 0 :(得分:4)

在对这个同样的问题猛烈抨击之后,我了解了这件事的可悲事实:

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

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

来支持
  

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

因此,没有什么好方法可以简单地将时区“追加”到时间戳。但我承诺,这并不是那么可怕!这只是意味着你需要另一个专栏。

我的(相当简单)建议的解决方案:

  1. 将时区存储在字符串列中(粗略,我知道)。
  2. 不要覆盖to_s,只需写一个getter。
  3. 假设您需要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

    希望这有帮助!

答案 1 :(得分:2)

我建议将它们保存在iso8601中

这将允许您:

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

  • 在两种情况下使用相同的存储格式和offset 无

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

从Ruby方面来看,它就像

一样简单
Time.now.iso8601
Time.now.utc.iso8601

ActiveRecord应与转换无缝协作。

此外,大多数API都使用此格式(谷歌),因此最适合跨应用程序兼容。

postgresql的

to_char()应该为您提供正确的格式,以防止出现默认设置的打嗝。

答案 2 :(得分:1)

正如您所建议的那样,一种方法是覆盖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显示正确的区域。