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

となるのかが分かりません。そんなのを隠すような記述がみつからないんですけどー。