通常,处理日期时的最佳实践是将它们存储在UTC中,并转换回用户在应用程序层中期望的任何内容。
这不一定适用于将来的日期,尤其是当日期/时间特定于用户所在的时区时。基于本地时间的计划需要存储本地时间。
在我的实例中,有一个属性是一个时间戳,其中包含未来事件start_time,与现在或过去的其他所有内容(包括created_at
和updated_at
时间戳)相比。
为什么
此特定字段是用户选择时间的未来事件的时间戳。
对于未来的事件,最佳做法似乎是不存储 UTC。
开发人员可以保存用户希望我们节省的内容:墙时间,而不是以 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
为此,我设置:
config/application.rb
config.active_record.default_timezone = :local
config.time_zone = 'UTC'
app/model/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
回调来进行转换。
config/initializers/time.rb
class Time
def time_to_i
self.strftime('%Y%m%d%H%M').to_i
end
end
app/model/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
理想的解决方案
我希望能够在数据库中以自然数据类型存储时间戳,这样查询就不会在我的控制器中变得混乱,但我无法弄清楚如何存储无法转换的"墙时间"。
其次是最好的,如果有必要,我会选择整数选项。
其他人如何处理这个问题?我错过了什么?特别是使用上面的"整数转换"选项,我使事情变得比它们需要的要复杂得多。
我建议你仍然使用第一个选项,但有一些技巧:本质上,你可以关闭所需属性的时区转换,并使用自定义资源库来克服属性写入期间的转换。
该技巧将时间节省为假 UTC 时间。虽然从技术上讲,它有一个UTC区域(因为所有时间都保存在UTC的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 将时间保存在 11:55 的"假"UTC 区域中。
另请注意,从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 格式。
这听起来可能有点离谱,但是我已经在我最近负责的一个应用程序中处理了类似的问题 - 但另一方面 - 当我运行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:您可以提供一个选择列表(为了友好起见,默认为用户的本地偏移量),以允许他们提供指定其日期的区域,而不是隐藏字段。
更新 - 时区选择我做了一些研究,看起来已经有一个时区选择框的表单帮助程序。
我投票给最简单的路线,那就是保存到UTC。为原产国创建一个列,为"夏令时"设置谷歌提醒,如果智利决定停止使用夏令时或以某种疯狂的方式更改它,您可以通过查询智利用户的数据库并使用脚本相应地调整他们的日期来适应。然后,您可以将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}