2013年8月18日日曜日

ActiveRecord::Core.generated_feature_methods が何をしているか?

今日参加させて頂いたKobe Rubyist Meetup 1stで話題になった、
ActiveRecord::Core.generated_feature_methods の意味が良く分からない問題を追跡してみました。

■疑問


@sutetotanuki さんより下記のコードの意味が分からないとの質問がありました。
以前から私も良く分からないコードだな~と思っていたのですが、理由を良く知りませんでした。
 97       def generated_feature_methods
 98         @generated_feature_methods ||= begin
 99           mod = const_set(:GeneratedFeatureMethods, Module.new)
100           include mod
101           mod
102         end
103       end
moduleをその場で生成しincludeしているだけ。何の役に立つのか?

■調査した結果


generated_feature_methods を呼び出しているのは、少し上の initialize_generated_modules メソッド とassociations/builder系 と nested_attributes.rb です。

まず initialize_generated_modules メソッドは

activerecord/lib/active_record/core.rb より
 91       def initialize_generated_modules
 92         super
 93
 94         generated_feature_methods
 95       end
となっており、generated_feature_methods を呼び出す前に、さらに上位のメソッドを呼び出しており、 その中で@generated_attribute_methodsが生成されています。

activerecord/lib/active_record/attribute_methods.rb より
 62       def initialize_generated_modules # :nodoc:
 63         @generated_attribute_methods = Module.new { extend Mutex_m }
 64         @attribute_methods_generated = false
 65         include @generated_attribute_methods
 66       end
これにより generated_attribute_methods が返すModuleは、generated_feature_methodsが返すModuleより継承ツリー上、必ず上位になります。

一方でassociations/builder系 と nested_attributes.rbでは、アクセサを生成する為のmoduleとして利用されている事がわかります。

activerecord/lib/active_record/associations/builder/association.rb より
 85     def define_accessors(model)
 86       mixin = model.generated_feature_methods
 87       define_readers(mixin)
 88       define_writers(mixin)
 89     end
これにより属性用のメソッドは、アソシエーション用のメソッドより継承ツリーでは上位で生成される。言い換えるとアソシエーション用のメソッドが優先して呼び出されるようにしたいという意図が見えると思います。

実際、関係がありそうなコミット
を見てみると、そのようなコミットコメントが書かれています。
Instead of generating association methods directly in the model
class, they are generated in an anonymous module which
is then included in the model class. There is one such module
for each association. The only subtlety is that the
generated_attributes_methods module (from ActiveModel) must
be forced to be included before association methods are created
so that attribute methods will not shadow association methods.
 テストケースには、さらによく分かるコードが残っています。
333   def test_association_methods_override_attribute_methods_of_same_name
334     assert_equal(developers(:david), computers(:workstation).developer)
335     # this next line will fail if the attribute methods module is generated lazily
336     # after the association methods module is generated
337     assert_equal(developers(:david), computers(:workstation).developer)
338     assert_equal(developers(:david).id, computers(:workstation)[:developer])
339   end
なるほど!深い理由がある事が分かりましたw