天天看点

Rails源码研究之ActiveRecord:二,Associations

今天学习一下ActiveRecord的Associations相关的源码,即了解一下我们常用的has_many、has_one、belongs_to、has_and_belongs_to_many的原理

1,activerecord-1.15.3\lib\active_record\associations.rb:

[code]

require 'active_record/associations/association_proxy'

require 'active_record/associations/association_collection'

require 'active_record/associations/belongs_to_association'

require 'active_record/associations/belongs_to_polymorphic_association'

require 'active_record/associations/has_one_association'

require 'active_record/associations/has_many_association'

require 'active_record/associations/has_many_through_association'

require 'active_record/associations/has_and_belongs_to_many_association'

require 'active_record/deprecated_associations'

module ActiveRecord

module Associations

module ClassMethods

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)

else

add_multiple_associated_save_callbacks(reflection.name)

add_association_callbacks(reflection.name, reflection.options)

collection_accessor_methods(reflection, HasManyAssociation)

end

add_deprecated_api_for_has_many(reflection.name)

end

def has_one(association_id, options = {})

reflection = create_has_one_reflection(association_id, options)

module_eval do

after_save <<-EOF

association = instance_variable_get("@#{reflection.name}")

if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)

association["#{reflection.primary_key_name}"] = id

association.save(true)

end

EOF

end

association_accessor_methods(reflection, HasOneAssociation)

association_constructor_method(:build, reflection, HasOneAssociation)

association_constructor_method(:create, reflection, HasOneAssociation)

configure_dependency_for_has_one(reflection)

# deprecated api

deprecated_has_association_method(reflection.name)

deprecated_association_comparison_method(reflection.name, reflection.class_name)

end

def belongs_to(association_id, options = {})

if options.include?(:class_name) && !options.include?(:foreign_key)

::ActiveSupport::Deprecation.warn(

"The inferred foreign_key name will change in Rails 2.0 to use the association name instead of its class name when they differ. When using :class_name in belongs_to, use the :foreign_key option to explicitly set the key name to avoid problems in the transition.",

caller)

end

reflection = create_belongs_to_reflection(association_id, options)

if reflection.options[:polymorphic]

association_accessor_methods(reflection, BelongsToPolymorphicAssociation)

module_eval do

before_save <<-EOF

association = instance_variable_get("@#{reflection.name}")

if association && association.target

if association.new_record?

association.save(true)

end

if association.updated?

self["#{reflection.primary_key_name}"] = association.id

self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s

end

end

EOF

end

else

association_accessor_methods(reflection, BelongsToAssociation)

association_constructor_method(:build, reflection, BelongsToAssociation)

association_constructor_method(:create, reflection, BelongsToAssociation)

module_eval do

before_save <<-EOF

association = instance_variable_get("@#{reflection.name}")

if !association.nil?

if association.new_record?

association.save(true)

end

if association.updated?

self["#{reflection.primary_key_name}"] = association.id

end

end

EOF

end

# deprecated api

deprecated_has_association_method(reflection.name)

deprecated_association_comparison_method(reflection.name, reflection.class_name)

end

if options[:counter_cache]

cache_column = options[:counter_cache] == true ?

"#{self.to_s.underscore.pluralize}_count" :

options[:counter_cache]

module_eval(

"after_create '#{reflection.name}.class.increment_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +

" unless #{reflection.name}.nil?'"

)

module_eval(

"before_destroy '#{reflection.name}.class.decrement_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +

" unless #{reflection.name}.nil?'"

)

end

end

def has_and_belongs_to_many(association_id, options = {}, &extension)

reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)

add_multiple_associated_save_callbacks(reflection.name)

collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)

old_method = "destroy_without_habtm_shim_for_#{reflection.name}"

class_eval <<-end_eval

alias_method :#{old_method}, :destroy_without_callbacks

def destroy_without_callbacks

#{reflection.name}.clear

#{old_method}

end

end_eval

add_association_callbacks(reflection.name, options)

# deprecated api

deprecated_collection_count_method(reflection.name)

deprecated_add_association_relation(reflection.name)

deprecated_remove_association_relation(reflection.name)

deprecated_has_collection_method(reflection.name)

end

private

def association_accessor_methods(reflection, association_proxy_class)

define_method(reflection.name) do |*params|

force_reload = params.first unless params.empty?

association = instance_variable_get("@#{reflection.name}")

if association.nil? || force_reload

association = association_proxy_class.new(self, reflection)

retval = association.reload

if retval.nil? and association_proxy_class == BelongsToAssociation

instance_variable_set("@#{reflection.name}", nil)

return nil

end

instance_variable_set("@#{reflection.name}", association)

end

association.target.nil? ? nil : association

end

define_method("#{reflection.name}=") do |new_value|

association = instance_variable_get("@#{reflection.name}")

if association.nil?

association = association_proxy_class.new(self, reflection)

end

association.replace(new_value)

unless new_value.nil?

instance_variable_set("@#{reflection.name}", association)

else

instance_variable_set("@#{reflection.name}", nil)

return nil

end

association

end

define_method("set_#{reflection.name}_target") do |target|

return if target.nil? and association_proxy_class == BelongsToAssociation

association = association_proxy_class.new(self, reflection)

association.target = target

instance_variable_set("@#{reflection.name}", association)

end

end

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

def collection_accessor_methods(reflection, association_proxy_class)

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

end

def association_constructor_method(constructor, reflection, association_proxy_class)

define_method("#{constructor}_#{reflection.name}") do |*params|

attributees = params.first unless params.empty?

replace_existing = params[1].nil? ? true : params[1]

association = instance_variable_get("@#{reflection.name}")

if association.nil?

association = association_proxy_class.new(self, reflection)

instance_variable_set("@#{reflection.name}", association)

end

if association_proxy_class == HasOneAssociation

association.send(constructor, attributees, replace_existing)

else

association.send(constructor, attributees)

end

end

end

def create_has_many_reflection(association_id, options, &extension)

options.assert_valid_keys(

:class_name, :table_name, :foreign_key,

:exclusively_dependent, :dependent,

:select, :conditions, :include, :order, :group, :limit, :offset,

:as, :through, :source, :source_type,

:uniq,

:finder_sql, :counter_sql,

:before_add, :after_add, :before_remove, :after_remove,

:extend

)

options[:extend] = create_extension_module(association_id, extension) if block_given?

create_reflection(:has_many, association_id, options, self)

end

def create_has_one_reflection(association_id, options)

options.assert_valid_keys(

:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as

)

create_reflection(:has_one, association_id, options, self)

end

def create_belongs_to_reflection(association_id, options)

options.assert_valid_keys(

:class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent,

:counter_cache, :extend, :polymorphic

)

reflection = create_reflection(:belongs_to, association_id, options, self)

if options[:polymorphic]

reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"

end

reflection

end

def create_has_and_belongs_to_many_reflection(association_id, options, &extension)

options.assert_valid_keys(

:class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,

:select, :conditions, :include, :order, :group, :limit, :offset,

:uniq,

:finder_sql, :delete_sql, :insert_sql,

:before_add, :after_add, :before_remove, :after_remove,

:extend

)

options[:extend] = create_extension_module(association_id, extension) if block_given?

reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)

reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))

reflection

end

end

end

end

[/code]

该文件主要定义了has_many、has_one、belongs_to、has_and_belongs_to_many这四种方法并将它们分别代理到各个具体的关联类

2,activerecord-1.15.3\lib\active_record\associations\association_collection.rb:

[code]

module ActiveRecord

module Associations

class AssociationCollection < AssociationProxy

def <<(*records)

result = true

load_target

@owner.transaction do

flatten_deeper(records).each do |record|

raise_on_type_mismatch(record)

callback(:before_add, record)

result &&= insert_record(record) unless @owner.new_record?

@target << record

callback(:after_add, record)

end

end

result && self

end

alias_method :push, :<<

alias_method :concat, :<<

def delete_all

load_target

delete(@target)

reset_target!

end

def delete(*records)

records = flatten_deeper(records)

records.each { |record| raise_on_type_mismatch(record) }

records.reject! { |record| @target.delete(record) if record.new_record? }

return if records.empty?

@owner.transaction do

records.each { |record| callback(:before_remove, record) }

delete_records(records)

records.each do |record|

@target.delete(record)

callback(:after_remove, record)

end

end

end

def clear

return self if length.zero? # forces load_target if hasn't happened already

if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all

destroy_all

else

delete_all

end

self

end

def destroy_all

@owner.transaction do

each { |record| record.destroy }

end

reset_target!

end

def create(attributes = {})

if attributes.is_a?(Array)

attributes.collect { |attr| create(attr) }

else

record = build(attributes)

record.save unless @owner.new_record?

record

end

end

protected

def find_target

records =

if @reflection.options[:finder_sql]

@reflection.klass.find_by_sql(@finder_sql)

else

find(:all)

end

@reflection.options[:uniq] ? uniq(records) : records

end

end

end

end

[/code]

AssociationCollection是HasOneAssociation、HasManyAssociation、HasManyThroughAssociation、BelongsToAssociation、HasAndBelongsToManyAssociation、

BelongsToPolymorphicAssociation这六种关联类的父类,其中定义了关联的<<、create、delete、clear等方法

3,activerecord-1.15.3\lib\active_record\reflection.rb:

[code]

module ActiveRecord

module Reflection

module ClassMethods

def create_reflection(macro, name, options, active_record)

case macro

when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many

reflection = AssociationReflection.new(macro, name, options, active_record)

when :composed_of

reflection = AggregateReflection.new(macro, name, options, active_record)

end

write_inheritable_hash :reflections, name => reflection

reflection

end

end

class MacroReflection

attr_reader :active_record

def initialize(macro, name, options, active_record)

@macro, @name, @options, @active_record = macro, name, options, active_record

end

end

class AggregateReflection < MacroReflection #:nodoc:

def klass

@klass ||= Object.const_get(options[:class_name] || class_name)

end

end

class AssociationReflection < MacroReflection #:nodoc:

def klass

@klass ||= active_record.send(:compute_type, class_name)

end

end

end

end

[/code]

该文件定义了reflection变量以及AggregateReflection、AssociationReflection类,我们在关联类的build方法里可以看到record = @reflection.klass.new(attributes)的调用

4,activerecord-1.15.3\lib\active_record\associations\has_many_association.rb:

[code]

module ActiveRecord

module Associations

class HasManyAssociation < AssociationCollection

def build(attributes = {})

if attributes.is_a?(Array)

attributes.collect { |attr| build(attr) }

else

record = @reflection.klass.new(attributes)

set_belongs_to_association_for(record)

@target ||= [] unless loaded?

@target << record

record

end

end

def count(*args)

if @reflection.options[:counter_sql]

@reflection.klass.count_by_sql(@counter_sql)

elsif @reflection.options[:finder_sql]

@reflection.klass.count_by_sql(@finder_sql)

else

column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)

options[:conditions] = options[:conditions].nil? ?

@finder_sql :

@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"

options[:include] = @reflection.options[:include]

@reflection.klass.count(column_name, options)

end

end

def find(*args)

options = Base.send(:extract_options_from_args!, args)

if @reflection.options[:finder_sql]

expects_array = args.first.kind_of?(Array)

ids = args.flatten.compact.uniq

if ids.size == 1

id = ids.first

record = load_target.detect { |record| id == record.id }

expects_array ? [ record ] : record

else

load_target.select { |record| ids.include?(record.id) }

end

else

conditions = "#{@finder_sql}"

if sanitized_conditions = sanitize_sql(options[:conditions])

conditions << " AND (#{sanitized_conditions})"

end

options[:conditions] = conditions

if options[:order] && @reflection.options[:order]

options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"

elsif @reflection.options[:order]

options[:order] = @reflection.options[:order]

end

merge_options_from_reflection!(options)

# Pass through args exactly as we received them.

args << options

@reflection.klass.find(*args)

end

end

protected

def method_missing(method, *args, &block)

if @target.respond_to?(method) || ([email protected]_to?(method) && Class.respond_to?(method))

super

else

create_scoping = {}

set_belongs_to_association_for(create_scoping)

@reflection.klass.with_scope(

:create => create_scoping,

:find => {

:conditions => @finder_sql,

:joins => @join_sql,

:readonly => false

}

) do

@reflection.klass.send(method, *args, &block)

end

end

end

end

end

end

[/code]

HasManyAssociation等具体的关联类里面就定义了一些自己的find、count、build等方法

总体说来,ActiveRecord首先定义has_many、belongs_to等方法,并利用反射得到这些方法的参数所代表的类,然后把对这些类的操作代理到具体的HasManyAssociation等关联类

了解了ActiveRecord的associations,我们可以轻松定制自己的关联方法和关联类