背景と要件
Meetingなどのイベントの時間をあらゆるタイムゾーンを考慮して保存したいという要求がありました。
たとえば、
Meeting1 は Tokyo で 11:00 〜開催される。
Meeting2 は UTC で 10:00〜開催される。
のようなことを表現したい。
実装手順
- RailsのAPIサーバーのdefaultのTimezoneを固定する。
- DBにTimezoneを保存する
- Timezoneを動的に変更するロジックをどこに挿入するかを決める。
- timzoneを考慮して、DBにtime型が保存されるか確認
- 保存する際に、timezone情報が無いstringが渡されないようにvalidation
RailsのAPIサーバーのdefaultのTimezoneを固定する。
まずRailsは考慮するタイムゾーンが複数存在します。
今回は、混乱をさけるため一旦以下すべてのTimezoneをUTCにしました。
- database
- activereocord
- system
DBにTimezoneを保存する
timezoneを変更する基準となる変数を保存します。
timezoneは ActiveSupport経由で変更するので、validな値が保存されるように以下のようなvalidationを書けます。
# tz :string
class Meeting < ApplicationRecord
validates :tz, inclusion: { in: ActiveSupport::TimeZone::MAPPING.values }, presence: true
end
APIでGET時に、timezoneをdynamicに変更するロジックをどこに挿入するかを決める。
APIで値を返すときに、動的にTimezoneを変更して時間を返すロジックをいれます。
今回の場合はSerialzerにくみこみました。このロジックは表示に関わるロジックなので、結果的にViewに近いLayerでいれることになりました。
# tz :string
class Meeting < ApplicationRecord
~ 省略 ~
def timezone
ActiveSupport::TimeZone.new(tz)
end
def use_timezone
Time.use_zone(timezone) do
yield
end
end
end
class MeetingSerializer
attribute :start_time do |record|
record.use_timezone do
record.start_time
end
end
end
timzoneを考慮して、DBにtime型が保存されるか確認
保存はtimezone情報がついている、stringを渡せば、ActiveRecordがいい感じにやってくれます。
# actieve_record のtimezone は UTC
[47] pry(main)> meeting
=> #<Meeting:0x00007ff2c7ad4618
id: "162b1ed7-5502-42f8-8bed-592d2dae7db1",
start_at: Mon, 05 Oct 2020 16:00:00 UTC +00:00,
created_at: Tue, 06 Oct 2020 06:28:39 UTC +00:00,
updated_at: Thu, 08 Oct 2020 02:11:54 UTC +00:00>
[48] pry(main)> meeting.start_at="2020-10-06T01:00:00+0900"
=> "2020-10-06T01:00:00+0900"
[49] pry(main)> meeting.start_at
=> Mon, 05 Oct 2020 16:00:00 UTC +00:00
保存する際に、timezone情報が無いstringが渡されないようにvalidation
以下のような、controllerのconcern moduleを作成し、before_action でvalidationをかませるようにしました。
module ValidateTimeFormat
class TimeFormatError < StandardError
end
extend ActiveSupport::Concern
included do
rescue_from ValidateTimeFormat::TimeFormatError, with: :render_time_format_error
end
TIME_FORMAT = '%FT%T%z'
def render_time_format_error(msg)
render json: { message: msg }, status: :bad_request
end
def validate_time_format(time_str)
DateTime.strptime(time_str, TIME_FORMAT).zone
rescue Date::Error
raise ValidateTimeFormat::TimeFormatError, :time_format_is_invalid
end
end
まとめ
RailsはTime系は考慮するポイントが多いイメージがありました。
今回実装したことで、完璧に理解しました()
twitterもフォローよろしくおねがいします。 !