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.rb
にconfig.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解決プラグインでも作るしか?