タイムゾーンはどのように設定しておこうか...

最近、MacBookOSX 10.5 Leopardの使い方ばかり気になって調べていたが、久々にRails関連のこと。既に、最新バージョンは2.2だそうだが、自分の頭はまだ2.1なのであった。(すっかりObjectiveC&AppleScript脳になってしまった頭を、 RubyRails脳に戻すのにちょっと苦労してしまった...。)時間の取り扱いは最も基本的なことなのだが、いろいろ悩むことが多い。

実験環境

デフォルトタイムゾーンの変化

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)は以下のように実装されていた。つまり...
    • 何らかのtime_zoneの設定があれば(config.time_zone = 'xxxxx')、DBにはUTC時間で記録される。
    • time_zoneの設定が何も無ければ、DBにはローカル時間(Railsサーバーが稼働するOS環境の時間)で記録される。
#---------- /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ペルパーが表示する日時の初期値とか、保存とか、

  • Railsサーバーのconfig.time_zoneでの現在日時が初期値として表示された。
    • DBに保存する時には、UTC時間に変換される。
  • 上記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 %>

自分の作戦

以上の結果から、何らかのタイムゾーン(例:Tokyo)を設定して、DBにはUTC時間で統一して記録するようにしようかと。そうすれば...

  • DBから読み出す時には、Railsタイムゾーンに変換した時間を返してくれるので幸せ。
  • datetime_selectヘルパーから入力する時も、Railsタイムゾーンの時間をUTCに変換してからDBに保存してくれるので幸せ。
  • restful-authentication&aasmもDB保存はUTC時間で統一されるので幸せ。
  • タイムゾーンを変更することで、別の時間帯にも簡単に対応出来そう。

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と認識される。(時間がないので変換なし)
    • 人の一般常識的な概念からすると、無意識のうちに以下のように考えてしまう。

このように、date_selectペルパーの返す値やUTC時間の変換によっては、人の一般常識とギャップがあるので、よく考えて実装しないと予想外の間違いを誘発する可能性がある。(タイムゾーンに限った話ではないが、日時指定の検索をする場合、結構大変そう。)

参考ページ

以下のページがたいへん参考になりました。感謝です!