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 モジュールで定義されているが、長いので図では省略)
以上のことを頭に入れた上で、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() を参照