タイムゾーンはどのように設定しておこうか...
最近、MacBookやOSX 10.5 Leopardの使い方ばかり気になって調べていたが、久々にRails関連のこと。既に、最新バージョンは2.2だそうだが、自分の頭はまだ2.1なのであった。(すっかりObjectiveC&AppleScript脳になってしまった頭を、 Ruby&Rails脳に戻すのにちょっと苦労してしまった...。)時間の取り扱いは最も基本的なことなのだが、いろいろ悩むことが多い。
デフォルトタイムゾーンの変化
config/environment.rbでの設定
config.time_zone = 'UTC' >> ActiveRecord::Base.default_timezone => :utc
- time_zoneに'Tokyo'(何らかのtime_zone)を設定すれば、default_timezoneは:utcになった。
config.time_zone = 'Tokyo' >> ActiveRecord::Base.default_timezone => :utc
- time_zoneの設定がない場合、default_timezoneは:localになった。
#config.time_zone = 'UTC' #config.time_zone = 'Tokyo' >> ActiveRecord::Base.default_timezone => :local
コントローラーでの設定
- コントローラーごとにタイムゾーンを設定することも可能だ。
- ただし、config/environment.rbで、何らかのtime_zone(config.time_zone = 'xxxxx')を設定している場合に限られるようだ。
- config.time_zoneの設定が無い状態では、Time.zone = 'Tokyo'は無視され、常にローカル時間(Railsサーバーが稼働するOS環境の時間)で運用された。
# ---------- app/controllers/users_controller.rb ---------- class TodosController < ApplicationController Time.zone = 'Tokyo' ...(中略)...
Railsが実装するcreated_at(..._on)、updated_at(..._on)の場合
- 上記のような影響を受けながら、created_at(_on)、updated_at(_on)は以下のように実装されていた。つまり...
#---------- /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/timestamp.rb ---------- module ActiveRecord # Active Record automatically timestamps create and update operations if the table has fields # named created_at/created_on or updated_at/updated_on. # # Timestamping can be turned off by setting # <tt>ActiveRecord::Base.record_timestamps = false</tt> # # Timestamps are in the local timezone by default but you can use UTC by setting # <tt>ActiveRecord::Base.default_timezone = :utc</tt> module Timestamp def self.included(base) #:nodoc: base.alias_method_chain :create, :timestamps base.alias_method_chain :update, :timestamps base.class_inheritable_accessor :record_timestamps, :instance_writer => false base.record_timestamps = true end private def create_with_timestamps #:nodoc: if record_timestamps t = self.class.default_timezone == :utc ? Time.now.utc : Time.now #<----------UTCまたはローカル時間を設定 write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil? write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil? write_attribute('updated_at', t) if respond_to?(:updated_at) write_attribute('updated_on', t) if respond_to?(:updated_on) end create_without_timestamps end def update_with_timestamps(*args) #:nodoc: if record_timestamps && (!partial_updates? || changed?) t = self.class.default_timezone == :utc ? Time.now.utc : Time.now #<----------UTCまたはローカル時間を設定 write_attribute('updated_at', t) if respond_to?(:updated_at) write_attribute('updated_on', t) if respond_to?(:updated_on) end update_without_timestamps(*args) end end end
restful-authentication&aasmが実装するdeleted_at、activated_atの場合
- 一方、restful-authentication&aasmはdefault_timezoneに影響されず、常にUTC時間で記録されるようだ。(コード下部)
- DBにどこの時間で記録するかは、プラグインを作る人の実装の仕方で変化する可能性がある。(restful-authentication&aasmに限らず)
#---------- vendor/plugins/restful-authentication/lib/authorization/aasm_roles.rb ---------- module Authorization module AasmRoles unless Object.constants.include? "STATEFUL_ROLES_CONSTANTS_DEFINED" STATEFUL_ROLES_CONSTANTS_DEFINED = true # sorry for the C idiom end def self.included( recipient ) recipient.extend( StatefulRolesClassMethods ) recipient.class_eval do include StatefulRolesInstanceMethods include AASM aasm_column :state aasm_initial_state :initial => :pending aasm_state :passive aasm_state :pending, :enter => :make_activation_code aasm_state :active, :enter => :do_activate aasm_state :suspended aasm_state :deleted, :enter => :do_delete aasm_event :register do transitions :from => :passive, :to => :pending, :guard => Proc.new {|u| !(u.crypted_password.blank? && u.password.blank?) } end aasm_event :activate do transitions :from => :pending, :to => :active end aasm_event :suspend do transitions :from => [:passive, :pending, :active], :to => :suspended end aasm_event :delete do transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted end aasm_event :unsuspend do transitions :from => :suspended, :to => :active, :guard => Proc.new {|u| !u.activated_at.blank? } transitions :from => :suspended, :to => :pending, :guard => Proc.new {|u| !u.activation_code.blank? } transitions :from => :suspended, :to => :passive end end end module StatefulRolesClassMethods end # class methods module StatefulRolesInstanceMethods # Returns true if the user has just been activated. def recently_activated? @activated end def do_delete self.deleted_at = Time.now.utc #<----------常にUTC時間で記録される end def do_activate @activated = true self.activated_at = Time.now.utc #<----------常にUTC時間で記録される self.deleted_at = self.activation_code = nil end end # instance methods end end
datetime_selectペルパーが表示する日時の初期値とか、保存とか、
- 上記config.time_zoneの指定が無い場合、ローカル時間(Railsサーバーが稼働するOS環境の時間)での現在日時が初期値として表示された。
- DBに保存する時でも、ローカル時間そのままの状態。
<h1>New todo</h1> <% form_for(@todo) do |f| %> <%= f.error_messages %> <p> <%= f.label :body %><br /> <%= f.text_field :body %> </p> <p> <%= f.label :due %><br /> <%= f.datetime_select :due %> </p> <p> <%= f.label :done %><br /> <%= f.check_box :done %> </p> <p> <%= f.submit "Create" %> </p> <% end %> <%= link_to 'Back', todos_path %>
date_selectペルパーを利用する時の注意
- datetime_selectヘルパーと同様に考えたくなるが、時間の概念が変わってくるので注意が必要。
- 初期値は、現在日時の時間部分が切り捨てられた日付として表示される。
- 例:この日記の日時であれば、2008-12-24 になる。
- 初期値は、現在日時の時間部分が切り捨てられた日付として表示される。
-
- config.time_zone = 'Tokyo'の場合...
- DBがdatetime型なら、Tokyo時間2008-12-24 00:00:00を、UTC時間2008-12-23 15:00:00として保存される。(UTC時間に変換される)
- DBがdate型なら、そのまま2008-12-24として保存され、読み出す時も2008-12-24と認識される。(時間がないので変換なし)
- config.time_zone = 'Tokyo'の場合...
-
- 人の一般常識的な概念からすると、無意識のうちに以下のように考えてしまう。
- 「2008-12-24から」==「2008-12-24 00:00:00から」
- 「2008-12-24まで」==「2008-12-24 23:59:59.999...まで」
- 人の一般常識的な概念からすると、無意識のうちに以下のように考えてしまう。
このように、date_selectペルパーの返す値やUTC時間の変換によっては、人の一般常識とギャップがあるので、よく考えて実装しないと予想外の間違いを誘発する可能性がある。(タイムゾーンに限った話ではないが、日時指定の検索をする場合、結構大変そう。)