diff options
Diffstat (limited to 'activerecord/lib/active_record/reflection.rb')
-rw-r--r-- | activerecord/lib/active_record/reflection.rb | 176 |
1 files changed, 155 insertions, 21 deletions
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0995750ecd..2aec73bd5b 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -5,10 +5,12 @@ module ActiveRecord included do class_attribute :reflections + class_attribute :aggregate_reflections self.reflections = {} + self.aggregate_reflections = {} end - # Reflection enables to interrogate Active Record classes and objects + # \Reflection enables to interrogate Active Record classes and objects # about their associations and aggregations. This information can, # for example, be used in a form builder that takes an Active Record object # and creates input fields for all of the attributes depending on their type @@ -21,18 +23,24 @@ module ActiveRecord case macro when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(macro, name, scope, options, active_record) when :composed_of - reflection = AggregateReflection.new(macro, name, scope, options, active_record) + klass = AggregateReflection + end + + reflection = klass.new(macro, name, scope, options, active_record) + + if klass == AggregateReflection + self.aggregate_reflections = self.aggregate_reflections.merge(name => reflection) + else + self.reflections = self.reflections.merge(name => reflection) end - self.reflections = self.reflections.merge(name => reflection) reflection end # Returns an array of AggregateReflection objects for all the aggregations in the class. def reflect_on_all_aggregations - reflections.values.grep(AggregateReflection) + aggregate_reflections.values end # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). @@ -40,8 +48,7 @@ module ActiveRecord # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) - reflection = reflections[aggregation] - reflection if reflection.is_a?(AggregateReflection) + aggregate_reflections[aggregation] end # Returns an array of AssociationReflection objects for all the @@ -55,7 +62,7 @@ module ActiveRecord # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # def reflect_on_all_associations(macro = nil) - association_reflections = reflections.values.grep(AssociationReflection) + association_reflections = reflections.values macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections end @@ -65,8 +72,7 @@ module ActiveRecord # Invoice.reflect_on_association(:line_items).macro # returns :has_many # def reflect_on_association(association) - reflection = reflections[association] - reflection if reflection.is_a?(AssociationReflection) + reflections[association] end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. @@ -75,8 +81,13 @@ module ActiveRecord end end - # Abstract base class for AggregateReflection and AssociationReflection. Objects of + # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. + # + # MacroReflection + # AggregateReflection + # AssociationReflection + # ThroughReflection class MacroReflection # Returns the name of the macro. # @@ -95,7 +106,7 @@ module ActiveRecord # Returns the hash of options used for the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt> - # <tt>has_many :clients</tt> returns +{}+ + # <tt>has_many :clients</tt> returns <tt>{}</tt> attr_reader :options attr_reader :active_record @@ -176,6 +187,7 @@ module ActiveRecord def initialize(*args) super @collection = [:has_many, :has_and_belongs_to_many].include?(macro) + @automatic_inverse_of = nil end # Returns a new, unsaved instance of the associated class. +attributes+ will @@ -284,15 +296,31 @@ module ActiveRecord alias :source_macro :macro def has_inverse? - @options[:inverse_of] + @options[:inverse_of] || find_inverse_of_automatically end def inverse_of - if has_inverse? - @inverse_of ||= klass.reflect_on_association(options[:inverse_of]) + @inverse_of ||= if options[:inverse_of] + klass.reflect_on_association(options[:inverse_of]) + else + find_inverse_of_automatically end end + # Clears the cached value of +@inverse_of+ on this object. This will + # not remove the :inverse_of option however, so future calls on the + # +inverse_of+ will have to recompute the inverse. + def clear_inverse_of_cache! + @inverse_of = nil + end + + # Removes the cached inverse association that was found automatically + # and prevents this object from finding the inverse association + # automatically in the future. + def remove_automatic_inverse_of! + @automatic_inverse_of = false + end + def polymorphic_inverse_of(associated_class) if has_inverse? if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of]) @@ -361,7 +389,85 @@ module ActiveRecord options.key? :polymorphic end + VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] + INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + private + # Attempts to find the inverse association automatically. + # If it cannot find a suitable inverse association, it returns + # nil. + def find_inverse_of_automatically + if @automatic_inverse_of == false + nil + elsif @automatic_inverse_of.nil? + set_automatic_inverse_of + else + klass.reflect_on_association(@automatic_inverse_of) + end + end + + # Sets the +@automatic_inverse_of+ instance variable, and returns + # either nil or the inverse association that it finds. + # + # This method caches the inverse association that is found so that + # future calls to +find_inverse_of_automatically+ have much less + # overhead. + def set_automatic_inverse_of + if can_find_inverse_of_automatically?(self) + inverse_name = active_record.name.downcase.to_sym + + begin + reflection = klass.reflect_on_association(inverse_name) + rescue NameError + # Give up: we couldn't compute the klass type so we won't be able + # to find any associations either. + reflection = false + end + + if valid_inverse_reflection?(reflection) + @automatic_inverse_of = inverse_name + reflection + else + @automatic_inverse_of = false + nil + end + else + @automatic_inverse_of = false + nil + end + end + + # Checks if the inverse reflection that is returned from the + # +set_automatic_inverse_of+ method is a valid reflection. We must + # make sure that the reflection's active_record name matches up + # with the current reflection's klass name. + # + # Note: klass will always be valid because when there's a NameError + # from calling +klass+, +reflection+ will already be set to false. + def valid_inverse_reflection?(reflection) + reflection && + klass.name == reflection.active_record.try(:name) && + klass.primary_key == reflection.active_record_primary_key && + can_find_inverse_of_automatically?(reflection) + end + + # Checks to see if the reflection doesn't have any options that prevent + # us from being able to guess the inverse automatically. First, the + # <tt>inverse_of</tt> option cannot be set to false. Second, we must + # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations. + # Third, we must not have options such as <tt>:polymorphic</tt> or + # <tt>:foreign_key</tt> which prevent us from correctly guessing the + # inverse association. + # + # Anything with a scope can additionally ruin our attempt at finding an + # inverse, so we exclude reflections with scopes. + def can_find_inverse_of_automatically?(reflection) + reflection.options[:inverse_of] != false && + VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) && + !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } && + !reflection.scope + end + def derive_class_name class_name = name.to_s.camelize class_name = class_name.singularize if collection? @@ -393,7 +499,7 @@ module ActiveRecord delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection - # Gets the source of the through reflection. It checks both a singularized + # Returns the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. # # class Post < ActiveRecord::Base @@ -401,6 +507,15 @@ module ActiveRecord # has_many :tags, through: :taggings # end # + # class Tagging < ActiveRecord::Base + # belongs_to :post + # belongs_to :tag + # end + # + # tags_reflection = Post.reflect_on_association(:tags) + # tags_reflection.source_reflection + # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> + # def source_reflection @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first end @@ -414,10 +529,11 @@ module ActiveRecord # end # # tags_reflection = Post.reflect_on_association(:tags) - # taggings_reflection = tags_reflection.through_reflection + # tags_reflection.through_reflection + # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection - @through_reflection ||= active_record.reflect_on_association(options[:through]) + active_record.reflect_on_association(options[:through]) end # Returns an array of reflections which are involved in this association. Each item in the @@ -426,6 +542,17 @@ module ActiveRecord # The chain is built by recursively calling #chain on the source reflection and the through # reflection. The base case for the recursion is a normal association, which just returns # [self] as its #chain. + # + # class Post < ActiveRecord::Base + # has_many :taggings + # has_many :tags, through: :taggings + # end + # + # tags_reflection = Post.reflect_on_association(:tags) + # tags_reflection.chain + # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>, + # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>] + # def chain @chain ||= begin chain = source_reflection.chain + through_reflection.chain @@ -496,12 +623,19 @@ module ActiveRecord source_reflection.options[:primary_key] || primary_key(klass || self.klass) end - # Gets an array of possible <tt>:through</tt> source reflection names: + # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. + # + # class Post < ActiveRecord::Base + # has_many :taggings + # has_many :tags, through: :taggings + # end # - # [:singularized, :pluralized] + # tags_reflection = Post.reflect_on_association(:tags) + # tags_reflection.source_reflection_names + # # => [:tag, :tags] # def source_reflection_names - @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym } + @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq end def source_options |