今天学习一下ActiveRecord的Associations相关的源码,即了解一下我们常用的has_many、has_one、belongs_to、has_and_belongs_to_many的原理
1,activerecord-1.15.3\lib\active_record\associations.rb:
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
该文件主要定义了has_many、has_one、belongs_to、has_and_belongs_to_many这四种方法并将它们分别代理到各个具体的关联类
2,activerecord-1.15.3\lib\active_record\associations\association_collection.rb:
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
AssociationCollection是HasOneAssociation、HasManyAssociation、HasManyThroughAssociation、BelongsToAssociation、HasAndBelongsToManyAssociation、
BelongsToPolymorphicAssociation这六种关联类的父类,其中定义了关联的<<、create、delete、clear等方法
3,activerecord-1.15.3\lib\active_record\reflection.rb:
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
该文件定义了reflection变量以及AggregateReflection、AssociationReflection类,我们在关联类的build方法里可以看到record = @reflection.klass.new(attributes)的调用
4,activerecord-1.15.3\lib\active_record\associations\has_many_association.rb:
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
HasManyAssociation等具体的关联类里面就定义了一些自己的find、count、build等方法
总体说来,ActiveRecord首先定义has_many、belongs_to等方法,并利用反射得到这些方法的参数所代表的类,然后把对这些类的操作代理到具体的HasManyAssociation等关联类
了解了ActiveRecord的associations,我们可以轻松定制自己的关联方法和关联类