Active Recordの型変換はいつ実行されるのか？

上記の動作を知らずに実装していたらハマったのでメモします。
（ジャストなドキュメントを見つけられなかったためActive Recordのソースやその他記事等を確認しましたが、間違っている部分がありましたらご指摘ください）

# 前提

- Rails 5.2.3

`Task` というクラスがあり、 `tests` という属性を持っているとします。
`tests` はRails上では `ActiveSupport::TimeWithZone` として扱われます。
アプリケーションのデフォルトのタイムゾーンは `Tokyo` です。

```ruby:rails_console
[2] pry(main)> Task.columns_hash["tests"].type
=> :datetime
[5] pry(main)> Time.zone.name
=> "Tokyo"
```

# 疑問
以下のアクション( `TasksController#index` )が実行された場合に `# ??` の部分はどのような値（タイムゾーン）となるか？

```ruby:tasks_controller.rb
class TasksController < ApplicationController
  def index
    task = Task.new(tests: "2019/01/01")
    Time.use_zone("UTC"){ task.tests }
    puts task.tests # ??
  end
end
```

## 正解
正解は `Tue, 01 Jan 2019 00:00:00 UTC +00:00`
`Tokyo` のタイムゾーンになると思っていたのでハマった。

# Active Recordの型変換
## 事前知識

Active Recordでは属性として入力された値を型変換することがあります。例えば属性にInteger型の `Task.id` があった場合で、idにInteger型以外の値が入力された場合でも、以下のように型変換して属性値として扱います。

```ruby:rails_console
[1] pry(main)> task = Task.new
=> #<Task:0x00007f3a201cad70 id: nil, created_at: nil, updated_at: nil, tests: nil>
[2] pry(main)> task.id = "1"
=> "1"
[3] pry(main)> task.id
=> 1
[4] pry(main)> task.id = "2019/01/01"
=> "2019/01/01"
[5] pry(main)> task.id
=> 2019
```

## 型変換が実行されるタイミングは？
さて、上記の型変換が実行されるタイミングですが、これまで私は何となく、前述のケースでいえば以下(1)にようにモデルがインスタンス化されたときだと思っていました。しかし、今回調べたところ、正しくは、初めてその属性のアクセサメソッドが実行されたとき、のようです。
すなわち、以下(2)のタイミングのときに`task.tests` は TimeWithZone 型に変換されるようです。

```ruby
(1)    task = Task.new(tests: "2019/01/01")
(2)    Time.use_zone("UTC"){ task.tests }
```

では、 (1) ではどこまで処理が実行されるのかというと、アクセスメソッドを定義するまで、実行されるようです（他にも色々しているのでしょうが）。
以下、(1)(2)それそれが実行される流れを簡単にコードで追ってみようと思います。

## ソースコードを読んでみる
### Model.new すると起きること

`ActiveRecord` を継承したクラスが `new` される場合に実行されるコンストラクタは `ActiveRecord::Core` モジュール（includeされるクラスは `ActiveRecord::Base` クラス）のコンストラクタのようです。コードを抜粋して確認してみます。

```ruby:core.rb
    # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
    # attributes but not yet saved (pass a hash with key names matching the associated table column names).
    # In both instances, valid attribute keys are determined by the column names of the associated table --
    # hence you can't have attributes that aren't part of the table columns.
    #
    # ==== Example:
    #   # Instantiates a single new object
    #   User.new(first_name: 'Jamie')
    def initialize(attributes = nil)
      self.class.define_attribute_methods
      @attributes = self.class._default_attributes.deep_dup

      init_internals
      initialize_internals_callback

      assign_attributes(attributes) if attributes

      yield self if block_given?
      _run_initialize_callbacks
    end
```
https://github.com/rails/rails/blob/5-2-stable/activerecord/lib/active_record/core.rb

上記の

```ruby
self.class.define_attribute_methods
```

部分で属性のアクセサメソッドを定義していると想像できます。
さらにこの奥に進んでみると、`ActiveRecord::AttributeMethods` モジュールの中で `define_attribute_methods` が定義されていました。

```ruby:attribute_methods.rb  
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
      def define_attribute_methods # :nodoc:
        return false if @attribute_methods_generated
        # Use a mutex; we don't want two threads simultaneously trying to define
        # attribute methods.
        generated_attribute_methods.synchronize do
          return false if @attribute_methods_generated
          superclass.define_attribute_methods unless self == base_class
          super(attribute_names)
          @attribute_methods_generated = true
        end
      end
```
https://github.com/rails/rails/blob/5-2-stable/activerecord/lib/active_record/attribute_methods.rb

この中の

```ruby
 superclass.define_attribute_methods 
```

によって今度はActiveModelモジュールの方の同名のメソッドが呼ばれます。

```ruby:attribute_methods.rb
      def define_attribute_methods(*attr_names)
        attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
      end
```
https://github.com/rails/rails/blob/5-2-stable/activemodel/lib/active_model/attribute_methods.rb

この中での `define_attribute_method` メソッドがアクセサメソッドを定義していそうです。ここを追ってみると同じファイル内で以下のメソッドが定義されています。


```ruby:attribute_methods.rb
      def define_attribute_method(attr_name)
        attribute_method_matchers.each do |matcher|
          method_name = matcher.method_name(attr_name)

          unless instance_method_already_implemented?(method_name)
            generate_method = "define_method_#{matcher.method_missing_target}"

            if respond_to?(generate_method, true)
              send(generate_method, attr_name.to_s)
            else
              define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
            end
          end
        end
        attribute_method_matchers_cache.clear
      end
```
https://github.com/rails/rails/blob/5-2-stable/activemodel/lib/active_model/attribute_methods.rb

この中の `attribute_method_matchers` は `ActiveModel::AttributeMethods::ClassMethods::AttributeMethodMatcher` の配列となっており、これらは `<Atrribute名>_in_database` 等すべての属性に共通して使えるようにするメソッドのプレフィックス等が格納されていました。
今回は、それらの深追いはせず、属性名自体のアクセサメソッドの定義の動作を追います。
それについては、上記コードの

```ruby
send(generate_method, attr_name.to_s)
```
が呼ばれるようです。このとき引数は

```ruby
generate_method # => define_method_attribute
attr_name.to_s # => tests
```

の通りでした。
`define_method_attribute`　メソッドが実行されるので、追ってみると以下で定義されていました。

```ruby:read.rb
          def define_method_attribute(name)
            safe_name = name.unpack("h*".freeze).first
            temp_method = "__temp__#{safe_name}"

            ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
            sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key

            generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
              def #{temp_method}
                #{sync_with_transaction_state}
                name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
                _read_attribute(name) { |n| missing_attribute(n, caller) }
              end
            STR

            generated_attribute_methods.module_eval do
              alias_method name, temp_method
              undef_method temp_method
            end
          end
```
https://github.com/rails/rails/blob/5-2-stable/activerecord/lib/active_record/attribute_methods/read.rb

上記により、 [module_eval](https://ref.xaio.jp/ruby/classes/module/class_eval)によりメソッドが動的に定義されている様子がわかります。

### 型変換が実行されるタイミング
さて、上記によりモデルが new されてアクセサメソッドが定義されるまでの流れは分かりました。話を戻して型変換が実際に実行される処理を確認してみたいと思います。すなわち、今回の例では(2)を実行すると起きること、です。

```ruby
(1)    task = Task.new(tests: "2019/01/01")
(2)    Time.use_zone("UTC"){ task.tests }
```

(2)の `task.tests` （＝Model.newしたときに動的に定義されたアクセサメソッド）を実行すると、上記で確認したように以下メソッドの内容が実行されることになります。

```ruby:read.rb
              def #{temp_method}
                #{sync_with_transaction_state}
                name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
                _read_attribute(name) { |n| missing_attribute(n, caller) }
              end
```
https://github.com/rails/rails/blob/5-2-stable/activerecord/lib/active_record/attribute_methods/read.rb

`_read_attribute(name)` が属性値を読み込んで値を返答している箇所と考えられるのでこの部分を追ってみると、同ファイルに定義されています。

```ruby:read.rb
        def _read_attribute(attr_name) # :nodoc:
          @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
        end
```
https://github.com/rails/rails/blob/5-2-stable/activerecord/lib/active_record/attribute_methods/read.rb

`@attributes` は `ActiveModel::AttributeSet` クラスです。
`fetch_value` メソッドを追ってみると、以下で定義されていました。

```ruby:attribute_set.rb
      def fetch_value(name)
        self[name].value { |n| yield n if block_given? }
      end
```
https://github.com/rails/rails/blob/5-2-stable/activemodel/lib/active_model/attribute_set.rb

`self[name].value` は今回はWebブラウザからリクエストを送信しており `ActiveModel::Attribute::FromUser` でした。
ここで実行される `value` メソッドを追うと以下の定義されていました。

```ruby:attribute.rb
    def value
      # `defined?` is cheaper than `||=` when we get back falsy values
      @value = type_cast(value_before_type_cast) unless defined?(@value)
      @value
    end
```
https://github.com/rails/rails/blob/5-2-stable/activemodel/lib/active_model/attribute.rb

さて、上記まで来るとかなり具体的なな処理が見えてきたように思います。
すなわち、

- `type_cast` メソッドで元々の入力値（ `value_before_type_cast` 、今回では `"2019/01/01"`）をキャストしている
- 一度キャストして返答した値は再びキャストしない（ `unless defined?(@value)` ）

と考えてよいでしょう。せっかくなので `type_cast` ももう少し追ってみようと思います。同ファイル内に以下の定義があります。

```ruby:attribute.rb
      class FromUser < Attribute # :nodoc:
        def type_cast(value) 
          type.cast(value)
        end
```
https://github.com/rails/rails/blob/5-2-stable/activemodel/lib/active_model/attribute.rb

`type.class` は `ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter` です。そのクラスの `cast` メソッドを確認してみると以下の定義が見つかります。

```ruby:time_zone_conversion.rb
        def cast(value)
          return if value.nil?

          if value.is_a?(Hash)
            set_time_zone_without_conversion(super)
          elsif value.respond_to?(:in_time_zone)
            begin
              super(user_input_in_time_zone(value)) || super
            rescue ArgumentError
              nil
            end
          else
            map_avoiding_infinite_recursion(super) { |v| cast(v) }
          end
        end
```
https://github.com/rails/rails/blob/5-2-stable/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb

今回 `value` は `"2019/01/01"` であり、Railsでは文字列が日時への変換対象のフォーマットを満たしていれば [in_time_zoneメソッド](https://apidock.com/rails/String/in_time_zone)で変換できるので、

```ruby
            begin
              super(user_input_in_time_zone(value)) || super
            rescue ArgumentError
              nil
            end
```
の処理が実行されます。`user_input_in_time_zone` メソッドを追うと、

```ruby:time_value.rb
        def user_input_in_time_zone(value)
          value.in_time_zone
        end
```
https://github.com/rails/rails/blob/5-2-stable/activemodel/lib/active_model/type/helpers/time_value.rb


が、実行されていました。なるほど型変換の実体は [in_time_zoneメソッド](https://apidock.com/rails/String/in_time_zone)が実行されていわけですね。すなわちその時の `Time.zone` のタイムゾーンにしたがって値が変換されると理解できます。

# まとめ

さて、最初に戻って、以下のアクション( `TasksController#index` )が実行された場合に `# ??` の部分が `Tue, 01 Jan 2019 00:00:00 UTC +00:00` となる理由は、

- (1) のタイミングでは各属性のアクセサメソッド等が定義されたたけで、実際にアクセサメソッド経由で取得される値の生成（型変換）はされていない
- (2) のタイミングではじめてアクセサメソッドが実行され、元々入力された値が（必要な場合に）型変換されて返答される
    - 型変換は String#in_time_zone で実行されており、このときのタイムゾーンはUTCのためUTCのタイムゾーンで値が生成される
- 一度型変換された値は二度目以降にアクセサメソッドが実行された後は再度型変換されないため、(3)のタイミングではタイムゾーンがTokyoであっても(2)で生成されたUTCの値が読み出される

```ruby:tasks_controller.rb
class TasksController < ApplicationController
  def index
    task = Task.new(tests: "2019/01/01") # (1)
    Time.use_zone("UTC"){ task.tests } # (2)
    puts task.tests # ?? (3)
  end
end
```

# 感想
長かった…。

# 参考
『メタプログラミングRuby』にも少しActive Recordの記載があった（ただし少しVersionが古いので今と違う部分があるかも）。

<div class="amazlet-box" style="margin-bottom:0px;"><div class="amazlet-image" style="float:left;margin:0px 12px 1px 0px;"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4873117437/kuredev-22/ref=nosim/" name="amazletlink" target="_blank"><img src="https://images-fe.ssl-images-amazon.com/images/I/5102wwx0VzL._SL160_.jpg" alt="メタプログラミングRuby 第2版" style="border: none;" /></a></div><div class="amazlet-info" style="line-height:120%; margin-bottom: 10px"><div class="amazlet-name" style="margin-bottom:10px;line-height:120%"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4873117437/kuredev-22/ref=nosim/" name="amazletlink" target="_blank">メタプログラミングRuby 第2版</a><div class="amazlet-powered-date" style="font-size:80%;margin-top:5px;line-height:120%">posted with <a href="http://www.amazlet.com/" title="amazlet" target="_blank">amazlet</a> at 19.10.12</div></div><div class="amazlet-detail">Paolo Perrotta <br />オライリージャパン <br />売り上げランキング: 85,250<br /></div><div class="amazlet-sub-info" style="float: left;"><div class="amazlet-link" style="margin-top: 5px"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4873117437/kuredev-22/ref=nosim/" name="amazletlink" target="_blank">Amazon.co.jpで詳細を見る</a></div></div></div><div class="amazlet-footer" style="clear: left"></div></div>