以UTC格式保存日期时间有时不准确

时间:2016-03-08 05:16:50

标签: ruby-on-rails timezone

通常,处理日期时的最佳做法是store them in UTC并转换回用户在应用程序层中所期望的任何内容。

这不一定适用于将来的日期,特别是日期/时间特定于用户时区的日期/时间。基于当地时间的时间表需要存储当地时间。

在我的实例中,有一个属性是包含未来事件的start_time的时间戳,与现在或过去的所有其他事件(包括created_atupdated_at时间戳)相比较。

为什么

此特定字段是用户选择时间的 future event 的时间戳。

对于未来的活动,最佳做法似乎是not to store UTC

  

开发人员可以保存用户期望我们保存的内容:停机时间,而不是将时间与时区一起保存。

当用户选择上午10点时,即使由于夏令时用户与UTC的偏移在创建和事件日期之间发生变化,也需要保持上午10点。

因此,2016年6月,如果用户在悉尼午夜2017年1月1日创建活动,该时间戳将作为2017-01-01 00:00存储在数据库中。创建时的偏移量为+10:00,但在事件发生时,它是+11:00 ..除非政府决定在此期间改变它。

同样明智的是,我希望我在2016年1月1日午夜在布里斯班创建的单独活动也将存储为2017-01-01 00:00。我将时区即Australia/Brisbane存储在一个单独的字段中。

在Rails中执行此操作的最佳实践方法是什么?

我尝试了很多选项却没有成功:

1。跳过转换

问题,这只会跳过读取的转换,而不是写入。

self.skip_time_zone_conversion_for_attributes = [:start_time]

2。将整个应用配置更改为使用config.default_timestamp :local

为此,我设置:

配置/ application.rb中

config.active_record.default_timezone = :local
config.time_zone = 'UTC'

应用/模型/ event.rb

...
self.skip_time_zone_conversion_for_attributes = [:start_time]
before_save :set_timezone_to_location
after_save :set_timezone_to_default

def set_timezone_to_location
  Time.zone = location.timezone
end

def set_timezone_to_default
  Time.zone = 'UTC'
end
...
坦率地说,我不确定这是做什么......但不是我想要的。

我认为这是有效的,因为我的布里斯班活动存储为2017-01-01 00:00但是当我为悉尼创建了一个新事件时,它被存储为2017-01-01 01:00,即使它在视图中正确显示为午夜。

既然如此,我担心悉尼事件仍然存在同样的问题,我正试图避免。

3。覆盖模型的getter和setter以存储为整数

我试图将事件start_time存储为数据库中的整数。

我尝试通过猴子修补Time类并添加before_validates回调来进行转换。

配置/初始化/ time.rb

class Time
  def time_to_i
    self.strftime('%Y%m%d%H%M').to_i
  end
end

应用/模型/ event.rb

before_validation :change_start_time_to_integer
def change_start_time_to_integer
  start_time = start_time.to_time if start_time.is_a? String
  start_time = start_time.time_to_i
end

# read value from DB
# TODO: this freaks out with an error currently
def start_time
  #take integer YYYYMMDDHHMM and convert it to timestamp
  st = self[:start_time]
  Time.new(
    st / 100000000,
    st / 1000000 % 100,
    st / 10000 % 100,
    st / 100 % 100,
    st % 100,
    0,
    offset(true)
  )
end

理想解决方案

我希望能够在数据库中以其自然数据类型存储时间戳,这样查询就不会在我的控制器中变得混乱,但我无法弄清楚如何存储不存在的“挂起时间”转换。

第二好,如果必须,我会选择整数选项。

其他人如何处理这件事?我错过了什么?特别是上面的“整数转换”选项,我使事情变得比它们需要的复杂得多。

3 个答案:

答案 0 :(得分:7)

我建议你仍然使用第一个选项,但是使用一点hack:实质上,你可以关闭所需属性的时区转换,并使用自定义setter来克服属性写入期间的转换。

该技巧将时间节省为假的UTC时间。虽然从技术上讲它有一个UTC区域(因为所有时间都以db格式保存在db中),但 按照定义 ,它应被解释为本地时间,无论当前时区如何

class Model < ActiveRecord::Base
  self.skip_time_zone_conversion_for_attributes = [:start_time]

  def start_time=(time)
    write_attribute(:start_time, time ? time + time.utc_offset : nil)
  end
end

让我们在rails控制台中测试一下:

$ rails c
>> future_time = Time.local(2020,03,30,11,55,00)
=> 2020-03-30 11:55:00 +0200

>> Model.create(start_time: future_time)
D, [2016-03-15T00:01:09.112887 #28379] DEBUG -- :    (0.1ms)  BEGIN
D, [2016-03-15T00:01:09.114785 #28379] DEBUG -- :   SQL (1.4ms)  INSERT INTO `models` (`start_time`) VALUES ('2020-03-30 11:55:00')
D, [2016-03-15T00:01:09.117749 #28379] DEBUG -- :    (2.7ms)  COMMIT
=> #<Model id: 6, start_time: "2020-03-30 13:55:00">

请注意,Rails将时间保存为“假”UTC区域中的11:55。

另请注意,从create返回的对象中的时间是错误的,因为在这种情况下区域是从“UTC”转换的。您必须计算并在设置start_time属性后每次重新加载对象,以便可以进行区域转换跳过:

>> m = Model.create(start_time: future_time).reload
D, [2016-03-15T00:08:54.129926 #28589] DEBUG -- :    (0.2ms)  BEGIN
D, [2016-03-15T00:08:54.131189 #28589] DEBUG -- :   SQL (0.7ms)  INSERT INTO `models` (`start_time`) VALUES ('2020-03-30 11:55:00')
D, [2016-03-15T00:08:54.134002 #28589] DEBUG -- :    (2.5ms)  COMMIT
D, [2016-03-15T00:08:54.141720 #28589] DEBUG -- :   Model Load (0.3ms)  SELECT  `models`.* FROM `models` WHERE `models`.`id` = 10 LIMIT 1
=> #<Model id: 10, start_time: "2020-03-30 11:55:00">

>> m.start_time
=> 2020-03-30 11:55:00 UTC

加载对象后,start_time属性正确,无论实际时区如何,都可以手动解释为本地时间。

我真的不明白为什么Rails的行为方式与skip_time_zone_conversion_for_attributes配置选项相同......

更新:添加阅读器

我们还可以添加一个阅读器,以便我们在当地时间自动解释保存的“假”UTC时间,而不会因时区变化而改变时间:

class Model < ActiveRecord::Base
  # interprets time stored in UTC as local time without shifting time
  # due to time zone change
  def start_time
    t = read_attribute(:start_time)
    t ? Time.local(t.year, t.month, t.day, t.hour, t.min, t.sec) : nil
  end
end

在rails控制台中测试:

>> m = Model.create(start_time: future_time).reload
D, [2016-03-15T08:10:54.889871 #28589] DEBUG -- :    (0.1ms)  BEGIN
D, [2016-03-15T08:10:54.890848 #28589] DEBUG -- :   SQL (0.4ms)  INSERT INTO `models` (`start_time`) VALUES ('2020-03-30 11:55:00')
D, [2016-03-15T08:10:54.894413 #28589] DEBUG -- :    (3.1ms)  COMMIT
D, [2016-03-15T08:10:54.895531 #28589] DEBUG -- :   Model Load (0.3ms)  SELECT  `models`.* FROM `models` WHERE `models`.`id` = 12 LIMIT 1
=> #<Model id: 12, start_time: "2020-03-30 11:55:00">

>> m.start_time
=> 2020-03-30 11:55:00 +0200

即。 start_time在本地时间正确解释,即使它以相同的小时和分钟存储,但以UTC格式存储。

答案 1 :(得分:1)

这可能听起来有点像,但是我已经处理了与我最近负责的应用程序类似的问题 - 但另一方面 - 当我运行ETL来为应用程序加载数据时,来自源的日期是存储在EST中。 Rails认为在提供数据时它是UTC,因此,我使用P / SQL将日期转换回UTC。我不希望这些日期与应用程序中的其他日期字段不同。

选项A 在这种情况下,您是否可以在创建时捕获用户时区,并将其作为隐藏字段发送回表单中?我还在学习RoR,所以我不确定是否采取“正确”的方式来做到这一点,但是现在我会做这样的事情:

示例(我对此进行了测试,并将在隐藏字段中提交偏移量(分钟)):

<div class="field">
  <%= f.hidden_field :offset %>
</div>

<script>
  utcDiff = new Date().getTimezoneOffset();
  dst_field = document.getElementById('timepage_offset');
  dst_field.value = utcDiff;
</script>

如果您随后发送utcDiff以及用户选择的日期,您可以在存储之前计算UTC日期。我想你可以将它添加到模型中,如果以后需要知道这些数据。

我认为无论如何做到这一点,总会有轻微的混淆区域,除非用户能够提供正确的信息,这导致我...

选项B: 您可以(而不是隐藏字段)提供选择列表(并且是友好的,默认为用户的本地偏移量),以允许它们提供指定其日期的区域。

更新 - TimeZone select 我做了一些研究,看起来已经有了一个时区选择框的表单助手。

答案 2 :(得分:0)

我投票选出了最简单的路线,并将其保存为UTC。为原产国创建一个列,为夏令时设置一个google警报&#34;如果智利决定停止使用夏令时或以某种疯狂的方式改变它,您可以通过查询数据库进行调整智利用户并使用脚本相应地调整日期。然后,您可以将Time.parse与日期,时间和时区一起使用。为了说明,以下是2016年3月13日夏令时和夏令时之前的结果:

Time.parse("2016-03-12 12:00:00 Pacific Time (US & Canada)").utc
=> 2016-03-12 20:00:00 UTC
Time.parse("2016-03-14 12:00:00 Pacific Time (US & Canada)").utc
=> 2016-03-14 19:00:00 UTC

这将为您提供已接受时区名称的列表:

timezones = ActiveSupport::TimeZone.zones_map.values.collect{|tz| tz.name}