Railsではデータベースのtime型を使用できます。例えば、PostgreSQLでは、時刻だけを扱うtime型を使用できます。本記事では、Railsでtime型カラム同士を比較する際に期待した判定結果にならないケースについて紹介します。
環境
- Rails: 7.1.3
- Ruby: 3.3.3
- PostgreSQL 16
データベースのタイムゾーンはUTC、RailsのタイムゾーンはAsia/Tokyoに設定しています。
また以下のように、Railsでtime型のカラムを定義しています。
create_table(:works) do |t|
t.time :time_from
t.time :time_to
end
マイグレーション実行後にtime without time zone型として追加されます。
ちなみにtime with time zone型はRailsではサポートしていないようで型変換がされません。timestamp型はtimestamptzとしてwith time zoneをサポートしている模様です。
バグになるコード
次のようなコードでは、期待通りに動作しない場合があります。
errors.add(:time_from, :invalid) if time_from > time_to
このコードは、time_from
とtime_to
がどちらもtime型であり、time_from
がtime_to
を上回っている場合にエラーとするものです。しかし、この判定が正しく動作しない場合があります。
何が問題か
time型はRailsで参照される際に、ActiveSupport::TimeWithZone
クラスとして扱われます。RubyやRailsには純粋なtime型を表すクラスが存在しないため、time型を参照する際、日時が含まれていない部分にデフォルトの日付(2000-01-01)がセットされます。
問題となるのは、データベースのタイムゾーンがUTC、Rails側のタイムゾーンがJSTの場合、00:00 ~ 08:59
の値がUTCからJSTに変換される際に日付が2000-01-01
から2000-01-02
に変更されることです。
例えば、08:59
を保存した場合、データベースでは23:59:00
で保存されます。そしてRailsでJSTに変換する際、日付をまたぐため、2000-01-02 08:59:00 +0900
のように扱われます。
このため、time_from
だけが日付をまたいで変換されている場合、時刻的にはtime_from
がtime_to
より小さくても、time_from > time_to
の判定がtrue
となります。
DB保存前は問題なし
データベースに保存する前のtime型同士の比較では、UTCからJSTへの変換が行われないため、日付が進むことはありません。
irb(main):020> Work.new(time_from: '8:59').time_from
=> Sat, 01 Jan 2000 08:59:00.000000000 JST +09:00
そのため、入力フォームなどで新規レコードを作成する際のバリデーションでは問題が発生しません。しかし、レコードを保存した後の更新処理で問題が発生する可能性があります。
対策
社内メンバーからも意見をもらい、以下のような対応が考えられると思います。
- time型カラム参照時に時刻部分だけ抽出した文字列に変換して文字列同士で比較する
- attributes APIでカスタムタイプを作成しtime型カラムを上書きする
その他に使ったことはないですが、Tod というtime型を扱えるgemがあるようです。
また、本記事で扱った問題をRailsのIssue #51679ですでに取り上げているようでした。こちらも参考になるかもしれません。
まとめ
Railsにおけるtime型同士の比較はデータベースとアプリケーションのタイムゾーンの違いにより、特定の時刻で日付が変更されることがあるため注意したほうが良さそうです。