has_manyのメソッドの戻り値
class User < ActiveRecord::Base belongs_to :group end class Group < ActiveRecord::Base has_many :users end
っていうモデルを作った場合、
>> g1 = Group.find(:first) => #<Group id: 1, ・・・> >> users = g1.users => [#<User id:1m ・・・>] >> users.class => Array
って出ます。しかも、
>> users.find(:all, :conditions => ["login like ?", "akm%"]) => [#<User id: 1, ・・・>]
っていうこともできちゃう。ただのArrayのオブジェクトだったらこんなことできる訳がない。裏があるはず、と思ってソースコードを読んでみました。
結論から言うと、なんと!実はusersはArrayのインスタンスではなかったのです!!
順番を追っていきましょう。まずはactiverecord-2.0.2/lib/activerecord/associations.rbのActiveRecord::Associations::ClassMethods#has_manyメソッド
def has_many(association_id, options = {}, &extension) reflection = create_has_many_reflection(association_id, options, &extension) configure_dependency_for_has_many(reflection) if options[:through] collection_reader_method(reflection, HasManyThroughAssociation) collection_accessor_methods(reflection, HasManyThroughAssociation, false) else add_multiple_associated_save_callbacks(reflection.name) add_association_callbacks(reflection.name, reflection.options) collection_accessor_methods(reflection, HasManyAssociation) end end
:throughオブションのことは忘れて、大雑把にいうと、
1. has_manyの引数から関連を定義する情報を持ったreflectionを生成(create_has_many_reflection)
2. :dependentオプションが含まれていた場合にはreflectionに設定を追加(configure_dependency_for_has_many)
3. validate_associated_records_for_xxxxのコールバックを定義(add_multiple_associated_save_callbacks)
4. before_add_for_xxxx, after_add_for_xxxx, before_remove_for_xxxx, after_remove_for_xxxxのコールバックを定義(add_association_callbacks)
5. コレクションっていうか配列としてのアクセサを定義(collection_accessor_methods)
って感じになっていて、今重要なのは5番目。
def collection_accessor_methods(reflection, association_proxy_class, writer = true) collection_reader_method(reflection, association_proxy_class) define_method("#{reflection.name}=") do |new_value| # Loads proxy class instance (defined in collection_reader_method) if not already loaded association = send(reflection.name) association.replace(new_value) association end define_method("#{reflection.name.to_s.singularize}_ids") do send(reflection.name).map(&:id) end define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| ids = (new_value || []).reject { |nid| nid.blank? } send("#{reflection.name}=", reflection.class_name.constantize.find(ids)) end if writer end
って感じになっていて、今知りたいのはreaderの話なので、collection_reader_methodを見てみると、
def collection_reader_method(reflection, association_proxy_class) define_method(reflection.name) do |*params| force_reload = params.first unless params.empty? association = instance_variable_get("@#{reflection.name}") unless association.respond_to?(:loaded?) association = association_proxy_class.new(self, reflection) instance_variable_set("@#{reflection.name}", association) end association.reload if force_reload association end end
ってなってる。重要なのはdefine_methodに渡されてるブロックの戻り値のassociation。association_proxy_classをnewして生成されている。これは何なの?って元をたどると、has_manyで指定してるHasManyAssociation!これがusersの正体だったのか!
という訳で、activerecord-2.0.2/lib/activerecord/associations/has_many_association.rbを見てみる。
module ActiveRecord module Associations class HasManyAssociation < AssociationCollection #:nodoc:
AssociationCollectionを継承してますね。
activerecord-2.0.2/lib/activerecord/associations/association_collection.rb
module ActiveRecord module Associations class AssociationCollection < AssociationProxy #:nodoc:
AssociationProxyを継承しとる。
activerecord-2.0.2/lib/activerecord/associations/association_proxy.rb
module ActiveRecord module Associations class AssociationProxy #:nodoc:
これで継承はおわり。
で、AssociationProxy#method_missingが定義されているので、インスタンスにないメソッドは、@targetっていうたぶん、実データであるArrayのメソッドが呼ばれるっぽい。
def method_missing(method, *args, &block) if load_target @target.send(method, *args, &block) end end
あ、よく見たらAssociationCollectionでもmethod_missingを定義してますね。
def method_missing(method, *args, &block) if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) super else @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) } end end
@reflection.klassっていうのは、関連の情報を持っているhas_manyのところで登場したreflectionのklassっていうメソッドで、関連先のモデルのクラスを指してます。これがなんかメソッド持ってたら、with_scopeかまして、そっちに投げちまえと。その中のload_targetから再度派生クラスの実データを取ってくるメソッド(find_target)を呼んで、実データをゲットしてる訳ですね。それ以外は@target自身のメソッドを呼ぶようにsuperしてます。
というわけで、
>> users.respond_to?(:target) => true >> [].respond_to?(:target) => false
となることまでは分かったのですが、なんで、
>> users.class => HasManyAssociation
じゃなくて
>> users.class => Array
となるのかが分かりません。そんなのを隠すような記述がみつからないんですけどー。