diff options
Diffstat (limited to 'activerecord/lib/active_record/reflection.rb')
-rw-r--r-- | activerecord/lib/active_record/reflection.rb | 116 |
1 files changed, 102 insertions, 14 deletions
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5de08953f9..e3e2cac042 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -265,7 +265,12 @@ module ActiveRecord false end - def through_reflection_foreign_key + def through_reflection_chain + [self] + end + + def through_conditions + [Array.wrap(options[:conditions])] end def source_reflection @@ -363,7 +368,7 @@ module ActiveRecord # Holds all the meta-data about a :through association as it was specified # in the Active Record class. class ThroughReflection < AssociationReflection #:nodoc: - delegate :association_primary_key, :foreign_type, :to => :source_reflection + delegate :foreign_key, :foreign_type, :association_foreign_key, :to => :source_reflection # Gets the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. @@ -392,6 +397,101 @@ module ActiveRecord @through_reflection ||= active_record.reflect_on_association(options[:through]) end + # Returns an array of AssociationReflection objects which are involved in this through + # association. Each item in the array corresponds to a table which will be part of the + # query for this association. + # + # If the source reflection is itself a ThroughReflection, then we don't include self in + # the chain, but just defer to the source reflection. + # + # The chain is built by recursively calling through_reflection_chain on the source + # reflection and the through reflection. The base case for the recursion is a normal + # association, which just returns [self] for its through_reflection_chain. + def through_reflection_chain + @through_reflection_chain ||= begin + if source_reflection.source_reflection + # If the source reflection has its own source reflection, then the chain must start + # by getting us to that source reflection. + chain = source_reflection.through_reflection_chain + else + # If the source reflection does not go through another reflection, then we can get + # to this reflection directly, and so start the chain here + chain = [self] + end + + # Recursively build the rest of the chain + chain += through_reflection.through_reflection_chain + + # Finally return the completed chain + chain + end + end + + # Consider the following example: + # + # class Person + # has_many :articles + # has_many :comment_tags, :through => :articles + # end + # + # class Article + # has_many :comments + # has_many :comment_tags, :through => :comments, :source => :tags + # end + # + # class Comment + # has_many :tags + # end + # + # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags, + # but only Comment.tags will be represented in the through_reflection_chain. So this method + # creates an array of conditions corresponding to the through_reflection_chain. Each item in + # the through_conditions array corresponds to an item in the through_reflection_chain, and is + # itself an array of conditions from an arbitrary number of relevant reflections. + def through_conditions + @through_conditions ||= begin + # Initialize the first item - which corresponds to this reflection - either by recursing + # into the souce reflection (if it is itself a through reflection), or by grabbing the + # source reflection conditions. + if source_reflection.source_reflection + conditions = source_reflection.through_conditions + else + conditions = [Array.wrap(source_reflection.options[:conditions])] + end + + # Add to it the conditions from this reflection if necessary. + conditions.first << options[:conditions] if options[:conditions] + + # Recursively fill out the rest of the array from the through reflection + conditions += through_reflection.through_conditions + + # And return + conditions + end + end + + # A through association is nested iff there would be more than one join table + def nested? + through_reflection_chain.length > 2 || + through_reflection.macro == :has_and_belongs_to_many + end + + # We want to use the klass from this reflection, rather than just delegate straight to + # the source_reflection, because the source_reflection may be polymorphic. We still + # need to respect the source_reflection's :primary_key option, though. + def association_primary_key + @association_primary_key ||= begin + # Get the "actual" source reflection if the immediate source reflection has a + # source reflection itself + source_reflection = self.source_reflection + while source_reflection.source_reflection + source_reflection = source_reflection.source_reflection + end + + source_reflection.options[:primary_key] || klass.primary_key + end + end + # Gets an array of possible <tt>:through</tt> source reflection names: # # [:singularized, :pluralized] @@ -429,10 +529,6 @@ module ActiveRecord raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end - unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil? - raise HasManyThroughSourceAssociationMacroError.new(self) - end - if macro == :has_one && through_reflection.collection? raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end @@ -440,14 +536,6 @@ module ActiveRecord check_validity_of_inverse! end - def through_reflection_primary_key - through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key - end - - def through_reflection_foreign_key - through_reflection.foreign_key if through_reflection.belongs_to? - end - private def derive_class_name # get the class_name of the belongs_to association of the through reflection |