ActiveRecord の歩き方 - Association 編(3)

コールグラフ

HasManyAssociation の詳細にすすむ前に、青木峰郎氏を見習ってここまでの調査結果をコールグラフにまとめてみよう。コールグラフとはメソッドの呼び出し関係をグラフに表したものだ。詳しくは、RHG 第4章「クラスとモジュール」を見てほしい。

ActiveRecord::Associations::ClassMethods#has_many (associations.rb)
  ActiveRecord::Associations::ClassMethods#create_has_many_reflection (associations.rb)
    ActiveRecord::Reflection::ClassMethods#create_reflection(reflection.rb)
  ActiveRecord::Associations::ClassMethods#collection_accessor_methods (associations.rb)
    ActiveRecord::Associations::ClassMethods#collection_reader_methods (associations.rb) (A)

という感じになる。いまわれわれは (A) の collection_reader_methods にいる。それでは、次に進もう。

HasManyAssociation

HasManyAssociation は association/has_many_association.rb で定義されている。実は、これは AssociationProxy と AssociationCollection のサブクラスになっている。継承関係を図で表すと以下のとおり。(注:これらのクラスは実際には ActiveRecord::Associations モジュールで定義されているが、長いので図では省略)



図1:Association クラス群の継承関係


以上のことを頭に入れた上で、HasManyAssociation がどうやって生成されるかコンストラクタを見てみる。

ActiveRecord::Associations::HasManyAssociation#initialize
def initialize(owner, reflection)
  super         # (A)
  construct_sql # (B)
end

たったこれだけである。(A) ではスーパークラス(実際には AssociationProxy) のコンストラクタがまず呼び出される。ここで行われているのは、実質的に owner, reflection をそれぞれ @owner, @reflection というインスタンス変数に入れているだけである。したがってここでは説明しない。おもしろいのは (B) の construct_sql である。

ActiveRecord::Associations::HasManyAssociation#construct_sql (縮約版)
def construct_sql
  @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
end

オリジナルには :finder_sql, :as, :counter_sql や @counter_sql の扱いが含まれていたが、とりあえず本筋に関係ないので全部削った。重要なのは、上の1行だけである。具体的な例で考えてみるとわかりやすい。繰り返し使っているブログシステムの Entry / Comment のモデルで考えてみよう。

▼モデルの例
class Entry < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
end

ここで次のようなコードを実行したと考える。

entry = Entry.find(1)      # (A)
comments = entry.comments  # (B)

(B) が実行された時点で、前回の調べたように Entry#comments の中で HasManyAssociation インスタンスが生成される。すると HasManyAssociation#initializer 経由で HasManyAssociation#construct_sql が起動される。この時点で、

変数名
@reflection.klass.table_name "comments"
@reflection.klass.primary_key_name "entry_id"
@owner.quoted_id*1 1

となるから(第1回参照)

     @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
# => @finder_sql = "comments.entry_id = 1"

ということになる。どうやら、entry.comments を実行するときに発行する SQL 文の WHERE 節をここで作っているようである。

*1:quoted_id は ここでは id とほぼ同じと考えてよい。詳しくは base.rb の ActiveRecord::Base#quoted_id() を参照