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 の値)