论坛首页 编程语言技术论坛

Rails源码研究之ActiveRecord:二,Associations

浏览 5950 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-06-20  
今天学习一下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) || (!@reflection.klass.respond_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,我们可以轻松定制自己的关联方法和关联类
   发表时间:2007-06-21  
光把代码罗列了一遍,什么都没讲,还不如自己看
0 请登录后投票
   发表时间:2007-06-21  
就是让你自己看
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics