ActiveRecord::Associations

负责ActiveRecord::Base的association部分的实现,也就是has_many, has_one等等。先看到我们平时用的has_many方法:

def has_many(name, scope = nil, **options, &extension)
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
  Reflection.add_reflection self, name, reflection
end

通过build方法创建了,reflection对象并且把reflection加入到record里。看到-name的操作,查了下是string的freeze方法,也是比较少见的写法了。

def add_reflection(ar, name, reflection)
  ar.clear_reflections_cache
  name = -name.to_s
  ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end

build主要是定义accessor,callback(主要包括dependent destroy之类的回调),以及validations。

def self.build(model, name, scope, options, &block)
  if model.dangerous_attribute_method?(name)
    raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
                         "this will conflict with a method #{name} already defined by Active Record. " \
                         "Please choose a different association name."
  end

  reflection = create_reflection(model, name, scope, options, &block)
  define_accessors model, reflection
  define_callbacks model, reflection
  define_validations model, reflection
  reflection
end
# frozen_string_literal: true

require "active_support/core_ext/enumerable"
require "active_support/core_ext/string/conversions"
require "active_support/core_ext/module/remove_method"
require "active_record/errors"

module ActiveRecord
  class AssociationNotFoundError < ConfigurationError #:nodoc:
    def initialize(record = nil, association_name = nil)
      if record && association_name
        super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
      else
        super("Association was not found.")
      end
    end
  end

  class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
    def initialize(reflection = nil, associated_class = nil)
      if reflection
        super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
      else
        super("Could not find the inverse association.")
      end
    end
  end

  class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
    def initialize(owner_class_name = nil, reflection = nil)
      if owner_class_name && reflection
        super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
      else
        super("Could not find the association.")
      end
    end
  end

  class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
    def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
      if owner_class_name && reflection && source_reflection
        super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
      else
        super("Cannot have a has_many :through association.")
      end
    end
  end

  class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
    def initialize(owner_class_name = nil, reflection = nil)
      if owner_class_name && reflection
        super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
      else
        super("Cannot have a has_many :through association.")
      end
    end
  end

  class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
    def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
      if owner_class_name && reflection && source_reflection
        super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
      else
        super("Cannot have a has_many :through association.")
      end
    end
  end

  class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
    def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
      if owner_class_name && reflection && through_reflection
        super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
      else
        super("Cannot have a has_one :through association.")
      end
    end
  end

  class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
    def initialize(owner_class_name = nil, reflection = nil)
      if owner_class_name && reflection
        super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
      else
        super("Cannot have a has_one :through association.")
      end
    end
  end

  class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
    def initialize(reflection = nil)
      if reflection
        through_reflection      = reflection.through_reflection
        source_reflection_names = reflection.source_reflection_names
        source_associations     = reflection.through_reflection.klass._reflections.keys
        super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => '. Is it one of #{source_associations.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')}?")
      else
        super("Could not find the source association(s).")
      end
    end
  end

  class HasManyThroughOrderError < ActiveRecordError #:nodoc:
    def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
      if owner_class_name && reflection && through_reflection
        super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through '#{owner_class_name}##{through_reflection.name}' before the through association is defined.")
      else
        super("Cannot have a has_many :through association before the through association is defined.")
      end
    end
  end

  class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
    def initialize(owner = nil, reflection = nil)
      if owner && reflection
        super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
      else
        super("Cannot modify association.")
      end
    end
  end

  class AmbiguousSourceReflectionForThroughAssociation < ActiveRecordError # :nodoc:
    def initialize(klass, macro, association_name, options, possible_sources)
      example_options = options.dup
      example_options[:source] = possible_sources.first

      super("Ambiguous source reflection for through association. Please " \
            "specify a :source directive on your declaration like:\n" \
            "\n" \
            "  class #{klass} < ActiveRecord::Base\n" \
            "    #{macro} :#{association_name}, #{example_options}\n" \
            "  end"
           )
    end
  end

  class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
  end

  class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
  end

  class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
    def initialize(owner = nil, reflection = nil)
      if owner && reflection
        super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
      else
        super("Through nested associations are read-only.")
      end
    end
  end

  class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
  end

  class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc:
  end

  # This error is raised when trying to eager load a polymorphic association using a JOIN.
  # Eager loading polymorphic associations is only possible with
  # {ActiveRecord::Relation#preload}[rdoc-ref:QueryMethods#preload].
  class EagerLoadPolymorphicError < ActiveRecordError
    def initialize(reflection = nil)
      if reflection
        super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
      else
        super("Eager load polymorphic error.")
      end
    end
  end

  # This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations
  # (has_many, has_one) when there is at least 1 child associated instance.
  # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
  class DeleteRestrictionError < ActiveRecordError #:nodoc:
    def initialize(name = nil)
      if name
        super("Cannot delete record because of dependent #{name}")
      else
        super("Delete restriction error.")
      end
    end
  end

  # See ActiveRecord::Associations::ClassMethods for documentation.
  module Associations # :nodoc:
    extend ActiveSupport::Autoload
    extend ActiveSupport::Concern

    # These classes will be loaded when associations are created.
    # So there is no need to eager load them.
    autoload :Association
    autoload :SingularAssociation
    autoload :CollectionAssociation
    autoload :ForeignAssociation
    autoload :CollectionProxy
    autoload :ThroughAssociation

    module Builder #:nodoc:
      autoload :Association,           "active_record/associations/builder/association"
      autoload :SingularAssociation,   "active_record/associations/builder/singular_association"
      autoload :CollectionAssociation, "active_record/associations/builder/collection_association"

      autoload :BelongsTo,           "active_record/associations/builder/belongs_to"
      autoload :HasOne,              "active_record/associations/builder/has_one"
      autoload :HasMany,             "active_record/associations/builder/has_many"
      autoload :HasAndBelongsToMany, "active_record/associations/builder/has_and_belongs_to_many"
    end

    eager_autoload do
      autoload :BelongsToAssociation
      autoload :BelongsToPolymorphicAssociation
      autoload :HasManyAssociation
      autoload :HasManyThroughAssociation
      autoload :HasOneAssociation
      autoload :HasOneThroughAssociation

      autoload :Preloader
      autoload :JoinDependency
      autoload :AssociationScope
      autoload :AliasTracker
    end

    def self.eager_load!
      super
      Preloader.eager_load!
    end

    # Returns the association instance for the given name, instantiating it if it doesn't already exist
    def association(name) #:nodoc:
      association = association_instance_get(name)

      if association.nil?
        unless reflection = self.class._reflect_on_association(name)
          raise AssociationNotFoundError.new(self, name)
        end
        association = reflection.association_class.new(self, reflection)
        association_instance_set(name, association)
      end

      association
    end

    def association_cached?(name) # :nodoc:
      @association_cache.key?(name)
    end

    def initialize_dup(*) # :nodoc:
      @association_cache = {}
      super
    end

    def reload(*) # :nodoc:
      clear_association_cache
      super
    end

    private
      # Clears out the association cache.
      def clear_association_cache
        @association_cache.clear if persisted?
      end

      def init_internals
        @association_cache = {}
        super
      end

      # Returns the specified association instance if it exists, +nil+ otherwise.
      def association_instance_get(name)
        @association_cache[name]
      end

      # Set the specified association instance.
      def association_instance_set(name, association)
        @association_cache[name] = association
      end

      module ClassMethods
        def has_many(name, scope = nil, **options, &extension)
          reflection = Builder::HasMany.build(self, name, scope, options, &extension)
          Reflection.add_reflection self, name, reflection
        end

        def has_one(name, scope = nil, **options)
          reflection = Builder::HasOne.build(self, name, scope, options)
          Reflection.add_reflection self, name, reflection
        end

        def belongs_to(name, scope = nil, **options)
          reflection = Builder::BelongsTo.build(self, name, scope, options)
          Reflection.add_reflection self, name, reflection
        end

        def has_and_belongs_to_many(name, scope = nil, **options, &extension)
          habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)

          builder = Builder::HasAndBelongsToMany.new name, self, options

          join_model = builder.through_model

          const_set join_model.name, join_model
          private_constant join_model.name

          middle_reflection = builder.middle_reflection join_model

          Builder::HasMany.define_callbacks self, middle_reflection
          Reflection.add_reflection self, middle_reflection.name, middle_reflection
          middle_reflection.parent_reflection = habtm_reflection

          include Module.new {
            class_eval <<-RUBY, __FILE__, __LINE__ + 1
              def destroy_associations
                association(:#{middle_reflection.name}).delete_all(:delete_all)
                association(:#{name}).reset
                super
              end
            RUBY
          }

          hm_options = {}
          hm_options[:through] = middle_reflection.name
          hm_options[:source] = join_model.right_reflection.name

          [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k|
            hm_options[k] = options[k] if options.key? k
          end

          has_many name, scope, hm_options, &extension
          _reflections[name.to_s].parent_reflection = habtm_reflection
        end
      end
  end
end

你可能感兴趣的:(ActiveRecord::Associations)