おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

Rails で DB の Time 型を扱う

Rails では、DB の Time 型 を扱うことができます。これは日付を必要とせず、時間だけを格納したいときに使います。

定義方法は、他の型と同じく add_column メソッドなどで time を指定するだけです。

add_column :users, :lunch_time, :time

ただ、これを使おうとするにはちょっとしたノウハウが必要です。

Ruby や Rails には、時間のみを扱うクラスはありません。DB の Time 型は Ruby の Time オブジェクトに変換されます。Time オブジェクトは年月日の情報を持っています。その際、タイムゾーンは utc (正確には ActiveRecord::Base.default_timezone で設定されたタイムゾーン) として扱われます。

lunch_time カラムに 12:00 が格納されていた場合、次のような Time オブジェクトが返ります。

user.lunch_time #=> 2000-01-01 12:00:00 UTC

日本向けのアプリケーションであれば、 Rails のタイムゾーンを次のように Tokyo に設定するケースはとても多いと思います。この場合、UTC と JST の違いで苦しむことになります。

config.time_zone = 'Tokyo'

具体的に例を見ていきましょう。

フォームからの入力を保存する

このケースは特に問題がありません。他の型と同じように格納できます。

time_field メソッドで input タグを作りフォームを submit すると、'12:00' のような文字列がサーバに投げられます。

user.lunch_time = '12:00'
user.save

で DB には 12:00 が格納されます。

現在時間と比較する

user.lunch_time #=> 2000-01-01 12:00:00 UTC

先程も書きましたが、Time オブジェクトは年月日の情報を含むので、JSTとの時間を比較するには工夫が必要です。現在時刻が、特定の User のランチタイムより前であるかを調べるには次のようにするとよいでしょう。

t = user.lunch_time
t.to_s(:time) > Time.zone.now.to_s(:time)

to_s(:time) とすると、タイムゾーンにかかわらず単純に時間だけが文字列として出力されます。しかしダサいですね…><

時間だけを扱うクラスを提供する gem もあります。これを使うともう少しマシな感じになるかもしれません。

jackc/tod

クエリを投げる

現在時刻以降にランチタイムを設定した User を取得したい場合、次のように書くと思ったとおりに動きません。

User.where('lunch_time >= ?', Time.zone.now)

理由は Time.zone.now が JST から UTC に変換されてからクエリが実行されるからです。

例えば今が日本時間の 11:00 であれば、次のような SQL が発行されるでしょう。

SELECT "users".* FROM "users" WHERE (lunch_time > '2015-05-28 2:00:00.000000')

これでは、2時より後にランチタイムを設定しているユーザを取得してしまいます(※)。想定している挙動と違いますね。これをうまく扱うにはどうしたらよいでしょうか。

※ PostgreSQL で試した場合は、日付の箇所は無視されるようです。sqlite は time 型でも日付が入るようで、結果は異なります。

次のようにするとうまく動きます。

User.where('lunch_time >= ?', Time.zone.now.to_s(:time))

次のようなクエリが発行されます。

SELECT "users".* FROM "users" WHERE (lunch_time > '11:00')

結局 to_s(:time) に頼ってしまいました。もっと綺麗に書く方法があるかもしれませんが、とりあえずはこれでうまく動きます。

Rails 5 ではこうなる

さて、time 型の微妙さは伝わりましたでしょうか。嬉しい事に、Rails 5 では time 型の挙動を変更することができるようになるようです。

  • Rails 5.0 で time 型をタイムゾーン対応に設定できる
  • Rails 5.1 で time 型はデフォルトでタイムゾーン対応になる
    • 設定で非対応にもできる

現在の rails の master ブランチを利用し、time 型のカラムを持つ User モデルで User.new などとすると次の warning メッセージが表示されます。

  Time columns will become time zone aware in Rails 5.1. This
  still causes `String`s to be parsed as if they were in `Time.zone`,
  and `Time`s to be converted to `Time.zone`.

  To keep the old behavior, you must add the following to your initializer:

  config.active_record.time_zone_aware_types = [:datetime]

  To silence this deprecation warning, add the following:

  config.active_record.time_zone_aware_types << :time

config.active_record.time_zone_aware_types に設定した型のカラムはタイムゾーン対応になるようです。

試しに config/application.rb に次の行を追加してみます。

config.time_zone = 'Tokyo'
config.active_record.time_zone_aware_types = [:datetime, :time]

time 型を入力すると、JST の時刻になっているのがわかります。

user.lunch_time = '12:00'
user.lunch_time #=> Sat, 01 Jan 2000 12:00:00 JST +09:00

DB には UTC 時間である "03:00:00" が格納されます。

というわけで、Rails 5.0 からは楽に time 型を取り扱えるようになりそうです。

参考

Time columns should support time zone aware attributes by sgrif · Pull Request #15726 · rails/rails