belongs_to アソシエーションの動作を読む

以下の動作が実際どのような動きをしているかメモ。

class User < ActiveRecord::Base
  belongs_to :club
end

User.column_names => ["id", "club_id" ....]
user = User.find(:first)
user.club => #<Club:0x.....

belongs_to の動作を読む

Rails は1.0.0 とかなり古い。読む中心のディレクトリは以下の通り。

/usr/local/lib/ruby/1.8/gems/activerecord-1.13.2/lib/active_record
associations.rb

ここに、belongs_to メソッドがある。

def belongs_to(association_id, options = {})
  # オプションが正しいか確認
  options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend)

  # ここで"club", "Club", "user_id" になる
  association_name, association_class_name, class_primary_key_name =
            associate_identification(association_id, options[:class_name], options[:foreign_key], false) --(1)

  require_association_class(association_class_name) --(2)

  # ここで、"club_id" になる
  association_class_primary_key_name = options[:foreign_key] || association_class_name.foreign_key
	
  association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) -- (3)
  association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
  association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)

.....


今回の場合は、以下のような形で呼ばれる。

  • belongs_to(:club)
(1) associate_identification が呼ばれる
def associate_identification(association_id, association_class_name, foreign_key, plural = true)
  if association_class_name !~ /::/
    association_class_name = type_name_with_module(
    association_class_name || 
      Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name) -- (1')
    )
  end

  primary_key_name = foreign_key || name.foreign_key -- (1'')
        
  return association_id.id2name, association_class_name, primary_key_name
end
  • associate_identification(:club, nil, nil, false) で呼ばれる
  • 最初のif にヒットする
  • (1') ではassociation_class_name はnil, plural はfalse により以下のようになる。
type_name_with_module(Inflector.camelize(association_id.id2name))
    • id2name はrubyのメソッド。シンボルに対する文字列を返す
    • type_name_with_module では今回の場合、引数「"club"」をそのまま返す
def type_name_with_module(type_name)
  self.name =~ /::/ ? self.name.scan(/(.*)::/).first.first + "::" + type_name : type_name
end
    • camelize され、association_class_name は「"Club"」になる
  • (1'')のname メソッドは、Reflection モジュールのメソッド。User#nameは「"user"」を返し、それに対してforeign_key を呼ぶと「"user_id"」 が返ってくる
  • return "club", "Club", "user_id"
(2) require_association_class が呼ばれる
def require_association_class(class_name)
  require_association(Inflector.underscore(class_name)) if class_name
end
  • require_association_class("Club") で呼ばれる
  • ロードパスの中から「club.rb」を探し、require する。これで、Club クラスが参照できるようになる。
(3)association_accessor_methods が呼ばれる
def association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
  define_method(association_name) do |*params|
    force_reload = params.first unless params.empty?
    association = instance_variable_get("@#{association_name}")
    if association.nil? or force_reload
      association = association_proxy_class.new(self,  
                association_name, association_class_name,
                association_class_primary_key_name, options)   -- (4)
      retval = association.reload    -- (6)
      unless retval.nil?
        instance_variable_set("@#{association_name}", association)
      else
        instance_variable_set("@#{association_name}", nil)
      return nil
    end
  end
  association
end
  • association_accessor_methods("club", "Club", "club_id", {}, BelongsToAssociation) で呼ばれる
  • ここで「User#club」が定義される

User#club の動作を読む

association_accessor_methods メソッドのdefine_method の部分がその実装になる
  • instance_variable_get はキャッシュのためなので最初によばれるときは機能しない
  • 最初に呼ばれた際にはif の条件にマッチする
(4)association = association_proxy_class.new .....
  • BelongsToAssociation.new(User インスタンス, "club", "Club", "club_id", {}) として呼ばれる
  • associations/belongs_to_association.rb
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
  super        
  construct_sql        
end
  • super により「associations/association_proxy.rb」 のinitialize が呼ばれる
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
  @owner = owner
  @options = options
  @association_name = association_name
  @association_class = eval(association_class_name, nil, __FILE__, __LINE__)
  @association_class_primary_key_name = association_class_primary_key_name

  proxy_extend(options[:extend]) if options[:extend]

  reset   -- (5)
end
  • initialize(User インスタンス, "club", "Club", "club_id", {}) で呼ばれる
  • @association_class がClub クラスになる
(5)reset
def reset
  @target = nil
  @loaded = false
end
  • キャッシュクリア
(6)reload
def reload
  reset
  load_target -- (7)
end
(7)load_target
def load_target
  # そのインスタンスが保存済み、または、未保存だけど外部キー(User#club_id)に値が入っている
  if !@owner.new_record? || foreign_key_present
    begin
      @target = find_target if not loaded? -- (8)
    rescue ActiveRecord::RecordNotFound
      reset
    end
  end
  @loaded = true if @target
  @target
end
(8)find_target
  • BelongsToAssociation モジュールに定義されている
    • 殆どのメソッドをProxy クラスに持たせ、実装が異なる部分を、各アソシエーションモジュールに定義している
def find_target
  if @options[:conditions]
    @association_class.find(
      @owner[@association_class_primary_key_name], 
      :conditions => interpolate_sql(@options[:conditions]),
      :include    => @options[:include]
    )
  else
    @association_class.find(@owner[@association_class_primary_key_name], :include => @options[:include])
  end
end
  • @options は{}、@association_class はClub クラス、@owner はUser インスタンスより、結果は次と同じになる。
Club.find(User インスタンスのclub_id の値)

まとめ

  • User インスタンスが未保存の場合も、club_id に値が入っていれば、User#club の結果はClub.find(User#club_id) の結果と同じになる
  • users テーブルにclub_id カラムがない場合、User#club は必ずnil
    • nil であってエラーにはならない