🕰️

Time.zone.now に書き換えれば良いってもんじゃねえぞ

2022/06/30に公開

誰が言い出したのか知らんが、 Time.nowTime.zone.now にしろ、という言説がまかり通っています。
rubocop-rails にも怒られる。
https://docs.rubocop.org/rubocop-rails/cops_rails.html#railstimezone

私は 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.nowTime.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 の中だけにしておきましょう。

脚注
  1. ところで、ブラウザはOSからタイムゾーン情報を取れるはずなのに、なぜそれをサーバーへ送る標準が存在しないのか、私は長らく疑問に思っている ↩︎

Discussion