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 endmoduleをその場で生成し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これにより属性用のメソッドは、アソシエーション用のメソッドより継承ツリーでは上位で生成される。言い換えるとアソシエーション用のメソッドが優先して呼び出されるようにしたいという意図が見えると思います。
実際、関係がありそうなコミット
- https://github.com/rails/rails/commit/61bcc318c865289d215e8e19618b9414bd07d1e8
- https://github.com/rails/rails/commit/7cba6a37848ba96b4decec885779fb309d71c339
テストケースには、さらによく分かるコードが残っています。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