Time.zone.now に書き換えれば良いってもんじゃねえぞ
誰が言い出したのか知らんが、 Time.now
を Time.zone.now
にしろ、という言説がまかり通っています。
rubocop-rails にも怒られる。
私は Time.zone.now
を使いません。 rubocop でも off にします。
Rails/TimeZone:
Enabled: false
なぜならデータベースにゾーンは保存されない
Post.update!(published_at: Time.now)
Post.update!(published_at: Time.zone.now)
この2つは全く同じ意味になります。つけても無意味ならつけてもいいじゃないかって?わざわざ .zone
がついていると、もしかしてゾーン情報も保存されるのかなと勘違いする人が出てきそうなので私はつけません。
そもそも Time.zone.now は何をしているのか
Time.now
と Time.zone.now
は class が違います。
irb(main):001:0> Time.now.class
=> Time
irb(main):002:0> Time.zone.now.class
=> ActiveSupport::TimeWithZone
簡単に言えば、 Time は「時刻」で、 TimeWithZone は「時刻 + ゾーン情報」です。
どこからか持ってきたゾーン情報を付与して返す、というのが Time.zone.now
の処理です。
付与するゾーン情報をどこから持ってくるのかというと Time.zone
です。
irb(main):003:0> Time.zone
=> #<ActiveSupport::TimeZone:0x00007f0df8707238 @name="Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil>
config/application.rb
とかで指定されがちなやつです。後述しますが、これはスレッドローカル変数になっています。
ゾーン情報はだれのもの
ruby 本体は環境変数TZがタイムゾーン設定となり、プロセス全体でそれを共有します。
システム全体で 1 つのタイムゾーンしか使わないのであれば、Rails プロセスの起動時にTZを設定すれば期待通りに動きます。
ワールドワイドな Web サービスを展開する場合はこれでは困ることになりました。
そこで Rails では Time.zone をリクエストローカル変数に し、 zone 情報が必要な時には TZ
ではなく Time.zone
を参照するようになりました。
多くの場合、 ゾーンはユーザーに紐づく情報 です。[1]
puma や unicorn でマルチスレッド/マルチプロセスでリクエストを処理しても Time.zone
はリクエストを受けてからレスポンスを返すまでの流れで値が保持されるようになっています。以下のようにすればリクエストごとにそのユーザーのタイムゾーンを使うことができます。
class ApplicationController < ActionController::Base
before_action do
Time.zone = current_user.timezone
end
end
このアプローチは、 controller, view 内で Time.zone を使っている分にはうまく機能します。しかし、 model 内に Time.zone への参照があって、それが controller 以外から呼ばれると。。。? 混乱の始まりです。
いつ問題が起きるのか
AWS lambda などからバッチ処理でメールを送るとします。そこには時刻を書き込む必要があるとします。
#{l post.published_at} に新規投稿がありました。
急におかしなことが起きます。タイムゾーンがおかしいぞ?時刻を保存しているところを確認すると以下のようになっているかもしれません。
Post.update!(published_at: Time.zone.now)
おかしい、ちゃんと .zone
をつけているのに。。。
そうです、この zone はデータベースに保存されていません。
controller の before_action で Time.zone
を指定するやり方は http response を返している限りは期待通りに動きますが、 Rake タスクやバッチ処理をする際におかしなことになります。どの zone を使えばいいのだっけ。。? 通常は User に紐づいているはずです。つまり、上の例では
post.user.timezone
を取ってきて使う、あるいはメールの受信者が post.user と異なる場合はその受信者の timezone を指定することになるでしょう。
ゾーン情報っていつ使うの
時刻同士の比較やソートにはゾーン情報は不要です。また、時刻を表示するのにも 3分前
のような相対表示を採用すればゾーン情報は不要です。 Rails には time_ago_in_words があるので使うと良いでしょう。
ほとんどの時刻処理に zone は使いません。
一番ハマりがちなのは、 「時刻->日付」「日付->時刻」の変換時 です。
post.published_at.in_time_zone(user.timezone).to_date
のように、その場でしかるべきタイムゾーンを明示するのをお勧めします。
まとめ
マルチタイムゾーンのシステムを作るつもりなら、 timezone は current_user から引けるようにし、 Time.zone を参照するのはせいぜい controller/view の中だけにしておきましょう。
-
ところで、ブラウザはOSからタイムゾーン情報を取れるはずなのに、なぜそれをサーバーへ送る標準が存在しないのか、私は長らく疑問に思っている ↩︎
Discussion