Quantcast
Channel: Ruby – ABC Blog
Viewing all articles
Browse latest Browse all 36

RailsでのTimeZone設定を復習する

$
0
0

最近実務で嵌ったことから理解不足を痛感。これを機に整理することにした。

標準時とタイムゾーン

日本とアメリカでは時差があり、日本では今が昼であってもアメリカは夜だったりします。

地球の自転や太陽との関係性など天体的な問題から地球全域で同じ時間帯を採用することができないため、各々の国や地域で採用する時刻系が異なっています。
ある地域で共通している時刻系をその国の標準時と呼びます。

標準時の適用範囲は、便宜上、国または地域という単位になっています。
太陽がちょうど真南にくることを意味する南中を基準に各地で正午を観測するような細かさだと、それぞれの経度ごとに標準時ができてしまいます。
それだといろいろ不便(例えば関東と東北でいちいち時差があったらめんどくさい)なので、地理的にある程度近い地域では共通の時刻を使いましょうということになってます。

この地域というのは必ずしも国とは限りません。
アメリカのような国土が広大な国だったり離島があったりする国では地方で採用している標準時が異なったりします。

逆に地理的に近ければ、国が違っても時差がない場合があります。
日本と韓国は時差がありません。日本ではJST、韓国ではKSTという標準時をそれぞれ採用していますが、これは呼び方が違っても時間帯は同じです。
このように共通の時間帯を使う地域全体をタイムゾーンと言います。

オフセットは協定世界時(UTC)

標準時やタイムゾーンはUTCを基準に何時間進んでいるか or 遅れているかで表現されます。
日本標準時(JST)はUTCより9時間進んでいるので +09:00 というような表記になります。

例えば、JSTが設定されているRailsアプリで時刻を出力と以下のような結果となります。

2.4.2 :003 > Time.zone_default
 => #<ActiveSupport::TimeZone:0x00007f9c009e5450 @name="Asia/Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>>
2.4.2 :004 > Time.zone.now
 => Sat, 13 Oct 2018 09:56:54 JST +09:00

で、それがどうしたの?

国(異なるタイムゾーン)をまたがって展開しているアプリであれば標準時やタイムゾーンの理解が必要になります。

タイムゾーンの設定というのはOSやRailsのアプリケーションで各々持っていて、あるオブジェクトではOSのタイムゾーンを参照、他方ではアプリケーションの設定を参照などややこしく、その上アプリとは別にデータベースのタイムゾーンも設定が別れていたりします。
このような事情から国際化が必要なアプリの実装では躓石となりやすい箇所です。

なのでこの記事では以下、具体的にRailsでのタイムゾーンの扱い方を整理していきます。

タイムゾーンに関する設定

コンピューターは時計を内蔵しているので、タイムゾーンを持っています。
前章でちらっと触れましたが、OSがタイムゾーンの設定を持っていたり、それとは別にアプリでタイムゾーンを扱うための設定も複数あったりします。

Railsアプリを実装する上で考慮すべき設定は以下です。

  1. OSのタイムゾーン
  2. アプリケーションのタイムゾーン
  3. アプリケーションがデータベースをどのタイムゾーンで扱うかの設定

以下、上記各々の設定場所とより小さい粒度での解説を表にしたものです。
※表中の「設定ファイル」はここでは config/application.rb で、既定値はrails 5.2.1でのものとします

  設定場所 詳細 既定値
1. OSのタイムゾーン (Linuxの場合)/etc/localtime Rubyの組み込みクラスがこの値を参照する
2. アプリケーションのタイムゾーン 設定ファイルの config.time_zone ActiveSupport::TimeWithZoneがこの設定値を参照する UTC
3. アプリケーションがデータベースを… 設定ファイルの config.active_record.default_timezone RailsがActiveRecordを通してデータベースに時刻を保存する、または取り出す際に値をどのタイムゾーンとして扱うかの設定 nil
設定ファイルの config.active_record.time_zone_aware_attributes ActiveRecordのインスタンスに時刻型のプロパティがあったときにどう扱うかの設定 nil

以下、各々に関して詳しく見ていきます。

1. OSのタイムゾーン

OS に関してはOS毎に設定方法が違っていたり、環境変数を参照したりということもありますがUNIX系なら以下のコマンドで参照できると思います。

$ date
2018年 10月13日 土曜日 11時37分44秒 JST

/etc/localtimeが基準ですが、環境変数TZが設定してあったりしたらそちらが優先されたりしますが、ここではRailsの設定を重点的に見たいのでこの辺は割愛します。

Rubyの組み込みクラスは上記のようなOSのタイムゾーンを参照しますので、実装する際はこの点は念頭に置いた方がいいでしょう。

例えばTimeクラスの以下のクラスメソッドでは仮にRailsの設定ファイル上、config.time_zoneが’UTC’であってもOSのタイムゾーンがJSTの場合は後者の時間で返ってきます。

2.5.0 :001 > Time.zone.name
 => "UTC" 
2.5.0 :002 > Time.new
 => 2018-10-13 13:02:59 +0900 
2.5.0 :003 > Time.now
 => 2018-10-13 13:02:58 +0900 
2.5.0 :004 > Time.local(2018, 10, 12)
 => 2018-10-12 00:00:00 +0900 

要するにアプリケーションのタイムゾーンとしてconfig.time_zoneで指定したものとOSのタイムゾーンが違っているときに、前者で設定したものを使いたいのに上記を呼び出すと意図した結果にならないので注意です。

ただ、Time.zoneやTime.currentみたいにActive Supportによって拡張されたものは挙動が違ってきます。

2.アプリケーションのタイムゾーン

ここでは上記小見出しは設定ファイルのconfig.time_zoneで設定するものを意味します。
これは具体的にはActiveSupport::TimeWithZoneで用いるタイムゾーンを指定する設定項目です。

config.time_zone = 'Asia/Tokyo'のようにJSTを使うように設定したら、OSのタイムゾーンが仮にUTCなどJST以外のものであってもJSTで時間を返してくれます。

2.5.0 :001 > Time.zone.name
 => "Asia/Tokyo"
2.5.0 :002 > Time.zone.now
 => Sat, 13 Oct 2018 14:41:44 JST +09:00

設定ファイルで指定したタイムゾーンで時間の処理をしたいなら基本的にActiveSupport::TimeWithZoneを使えばOKだと思います。
なお、Time.currentTime.zone.xxxxxx.in_time_zone 等のメソッドを使うと、Timeではなく、TimeWithZoneのインスタンスが返されるので注意してください。

2.5.0 :005 > Time.zone.now.class
 => ActiveSupport::TimeWithZone
2.5.0 :006 > Time.current.class
 => ActiveSupport::TimeWithZone
2.5.0 :007 > Time.zone.now.class
 => ActiveSupport::TimeWithZone
2.5.0 :011 > Time.now.in_time_zone.class
 => ActiveSupport::TimeWithZone
2.5.0 :012 > 

これらTimeWithZoneのインスタンスを返すものはActiveSupportによって実装されたものです。
また、3.days.agoのようなものもActiveSupportの実装ですから、これらもTimeWithZoneのインスタンスを返します。

2.5.0 :009 > 3.days.ago.class
 => ActiveSupport::TimeWithZone

TimeWithZoneのインスタンスはnewではなく、上記で例示したようなもので生成するのが望ましいらしいです。

TimeはTime.utcのようにUTCで処理できるメソッドがあるので、OSのタイムゾーン以外にUTCも扱えますが、扱えるタイムゾーンはこの2つだけです。
一方ActiveSupport::TimeWithZoneは様々なタイムゾーンを扱えます。

2.5.0 :013 > Time.now.in_time_zone('Hawaii')
 => Fri, 12 Oct 2018 19:55:08 HST -10:00

一般的にはタイムゾーンの扱いに長けているActiveSupport::TimeWithZoneを使った方がいいでしょう。
ただし、ActiveSupport::TimeWithZoneはRuby組み込みのメソッドで取得したUTCの時間を基準に、設定されているタイムゾーンの時間に変換するという仕組みらしいのでOSの時間がずれてたらActiveSupport::TimeWithZoneでタイムゾーンを考慮して算出する時刻もずれるので注意が必要です。

アプリケーションがデータベースをどのタイムゾーンで扱うかの設定

設定ファイルには前章のconfig.time_zone以外にも以下のタイムゾーン設定項目があります。

  • config.active_record.default_timezone
  • config.active_record.time_zone_aware_attributes

config.active_record.default_timezone

ActiveRecordがマッピングしたDBテーブルのレコードは、created_atやupdated_atのような時刻をプロパティとして保持することになります。
これらはTimeWithZoneのインスタンスです。

2.4.2 :010 > Admin.first.created_at.class
  Admin Load (0.5ms)  SELECT  `admins`.* FROM `admins` ORDER BY `admins`.`id` ASC LIMIT 1
 => ActiveSupport::TimeWithZone

この設定項目は、上記のようなActiveRecordでActiveSupport::TimeWithZoneを処理する際に基準とするタイムゾーンを指定するものです。

設定できる値は:utc or :localの2択です。
:utcの場合は、ActiveRecordのインスタンスが持っているTimeWithZoneの値をUTCに変換します。OSのタイムゾーンは考慮しません。
:localの場合はOSのタイムゾーンが使われます。

設定値は以下で確認できます。

> ActiveRecord::Base.default_timezone
 =&gt; :utc

なので、config.time_zone = 'Asia/Tokyo'のような設定のアプリでも、config.active_record.default_timezone = :utcであれば、ActiveRecordでレコード保存する場合に時刻はUTCに変換して保存されます。例えば、日本標準時では15:00の時点で保存するとDB上の記録は0:00となります。
findなどでインスタンス化するときは逆にUTCで保存されたレコードを日本標準時に変換してくれます(ActiveRecordのインスタンスだとcreated_atはActiveSupport::TimeWithZoneのインスタンスなので、config.time_zoneの設定値が適用されるわけです)

config.active_record.time_zone_aware_attributes

この項目を明示的にfalseにすると(要するにconfig.active_record.time_zone_aware_attributes = falseと記述すると)、ActiveRecordが時刻系のプロパティをActiveSupport::TimeWithZoneではなく、Timeで扱うようになります。

2.4.2 :002 > Admin.first.created_at.class
  Admin Load (0.5ms)  SELECT  `admins`.* FROM `admins` ORDER BY `admins`.`id` ASC LIMIT 1
 => Time

こういう振る舞いにしたいという明確な意図がないなら使うことはないと思います。ここではあまり考えないことにします。

実際やらかした例

2018/10/05 13:00 以降にxxしたいみたいな条件判定で、ローカルで以下のように時刻出してた。

2.5.0 :012 > Time.new(2018, 10, 5, 13).in_time_zone('Asia/Tokyo')
=> Fri, 05 Oct 2018 13:00:00 JST +09:00

ローカルはMacだからOSのタイムゾーンはJST。Time.newはOSのタイムゾーン参照する。

一方本番のサーバのOSのタイムゾーンはUTC。だから上記と結果が違う。

2.5.0 :003 > Time.new(2018, 10, 5, 13).in_time_zone('Asia/Tokyo')
 => Fri, 05 Oct 2018 22:00:00 JST +09:00

だってUTCでの 2018/10/05 13:00 にさらにJSTとの差分を追加してんだもん。そりゃ違うよ。

~~疲れてたから頭回らず、本番でコンソールとかも実行できなかったから気づくの遅れたのもあって~~ ←言い訳。Time.newの仕様とタイムゾーン設定の確認不足で失敗しただけ。
とりあえずこの記事あればもう同様のミスはないと思うが・・・

個人的に注意した方がいいと思ったこと

  • ActiveSupportがTimeクラスを拡張しているのでTimeのメソッド全てがOSのタイムゾーンを参照するTimeのインスタンスを返すとは限らない
  • config.active_record.default_timezone は途中で変えちゃいけない(例えばMySQLの場合DATETIME型はタイムゾーン情報も保存しているわけではないのでごちゃごちゃになる)
  • MySQLの場合は、SQLでNOW()関数を使う場合その時刻はOSのタイムゾーンが基準なので、ActiveRecordによって記録されているタイムゾーンがOSのものと一致してない場合、比較がずれる
  • 設定ファイルの既定値はRails自体をバージョンアップした時に変わってしまう可能性があるので明示的に指定した方がいい
  • config.active_record.time_zone_aware_attributes = falseは使わない

Viewing all articles
Browse latest Browse all 36

Latest Images

Trending Articles





Latest Images