: O. Yuanying

RailsでUglyになんとかTimeZoneをサポートする

概要

Rails のためのものぐさな Web アプリケーションの国際化手法の最後に「次回は Rails での TimeZone 対応についてのお話です」と書かれていたのでwktkしながら待っているのですが、その次回がなかなかやってこないので、仕方なく力技でなんとかしようと思う。

ユーザごとのタイムゾーン

とりあえず基本は「Railsレシピ」のレシピ49に詳しいのでそれを見るとする。

ただこれだけだと特に ActiveRecord とタイムゾーンのマッピングがいまいちスマートにできてません。。なんですよね。

今回はレシピ49と同じように簡単なリマインダーをサンプルにして考えてみる。

サンプルのモデル

Migreationファイル

#db/migrate/001_add_users_and_task_reminders_tables.rb
class AddUsersAndTaskRemindersTabes < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :name, ;string
      t.column :time_zone, :string
    end
    create_table :task_reminders do |t|
      t.column :user_id, :integer
      t.column :due_at, :datetime
      t.column :description, :text
    end
  end

  def self.down
    drop_table :users
    drop_table :task_reminders
  end
end

モデルファイル

#app/models/user.rb
class User < ActiveRecord::Base
  has_many :task_reminders, :order => 'due_at'
  composed_of :tz,
                           :class_name => 'TimeZone',
                           :mapping => %w(time_zone name)
end
#app/models/task_reminder.rb
class TaskReminder < ActiveRecord::Base
  belong_to :user
end

システムのタイムゾーンをUTCに統一する

システムのタイムゾーンはUTCに統一してやる。データベースに保存される時刻のタイムゾーンもUTC。

config/environment.rbconfig.active_record.default_timezone = :utcを追加してやる。

# config/environment.rb
Rails::Initializer.run do |config|
  # Make Active Record use UTC-base instead of local time
  config.active_record.default_timezone = :utc
end

ただ、このままだとユーザがタスクを追加しようとする時、フォームから入力する時刻/日付はUTCで入力しないとならない。

せっかくユーザごとにタイムゾーンを持たせているので、フォームから時刻/日付を入力する時にはユーザのタイムゾーンでの時刻を入力できるようにしたい。

そこでTaskReminderクラスにユーザのローカルの時刻を取得できるゲッターとセッターを設定してやる。

#app/models/task_reminder.rb
class TaskReminder < ActiveRecord::Base
  belong_to :user
  composed_of :due_at_local, :class_name => 'Time'
  
  def due_at_local=(time)
    self.due_at = time - self.user.tz.utc_offset
  end
  
  def due_at_local
    self.due_at + self.user.tz.utc_offset
  end
end

ここの肝は、composed_of :due_at_local, :class_name => 'Time'で、これを設定してやることでparamsからupdate_attributesを使ってdue_at_localを更新できるようになる。

ActiveRecord で TaskReminder インスタンスを検索する

問題はデータベースにdue_atがUTCで保存されているってことなんですよね。だから例えばユーザが8月20日のタスクを検索した時に、ローカルタイムで8月19日と8月20日のタスクが引っかかってきてしまう。

これに関してはどれもうまい解決策が全く思いつかなくて、全部環境依存になってしまう。

データベースをMySQL固定にしちゃっていいならば、CONVERT_TZ関数とかを使うしか無いんじゃないだろうか…。この場合はTimeZone#utc_offsetを'+09:00'とかの形式に変換しないとダメだけど。

もしくは環境依存を排除するようなTimeZone解決プラグインでも作るしか?