Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #6461: nested_has_many_through_updated.diff

File nested_has_many_through_updated.diff, 15.5 kB (added by obrie, 2 years ago)

Updated with a few more tests and some fixes.

  • activerecord/lib/active_record/associations/has_many_through_association.rb

    old new  
    155155 
    156156        # Build SQL conditions from attributes, qualified by table name. 
    157157        def construct_conditions 
    158           table_name = @reflection.through_reflection.table_name 
    159           conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value| 
     158          reflection, table_id = find_deepest_through(@reflection.through_reflection) 
     159          table_name = reflection.table_name 
     160          table_name += "_#{table_id}" if table_id 
     161 
     162          conditions = construct_quoted_owner_attributes(reflection).map do |attr, value| 
    160163            "#{table_name}.#{attr} = #{value}" 
    161164          end 
     165 
    162166          conditions << sql_conditions if sql_conditions 
    163167          "(" + conditions.join(') AND (') + ")" 
    164168        end 
     
    163167          "(" + conditions.join(') AND (') + ")" 
    164168        end 
    165169 
     170        # Look ahead through a reflection to find out where the deepest 
     171        # association is in the class that doesn't have a through relationship. 
     172        # For example, 
     173        #  
     174        #  class A < ActiveRecord::Base 
     175        #    has_many :bs 
     176        #    has_many :cs, :through => :bs 
     177        #    has_many :ds, :through => :cs 
     178        #    has_many :es, :through => ds 
     179        #  end 
     180        #  
     181        # Here, our :bs association is the deepest when going into any of the 
     182        # :through associations.  In addition to returning the :bs reflection, 
     183        # it will also return the table alias id for use in the database query. 
     184        # The reason for an alias id is that you could potentially go through 
     185        # the same table more than once.  In this case, you need to keep track 
     186        # of how many times you went through the table so that the proper alias 
     187        # can be applied to the table when running the query. 
     188        def find_deepest_through(reflection, table_ids = {@reflection.table_name => 1, reflection.table_name => 1}) 
     189          # Don't overwrite the original hash since this only a look-ahead 
     190          table_ids = table_ids.dup 
     191 
     192          if through_reflection = reflection.through_reflection 
     193            table_name = through_reflection.table_name 
     194            table_ids[table_name] = (table_ids[table_name] || 0) + 1 
     195 
     196            find_deepest_through(through_reflection, table_ids) 
     197          else 
     198            table_id = table_ids[reflection.table_name] || 1 
     199             
     200            # Only running into the table once means we don't need a table id 
     201            return reflection, (table_id > 1 ? table_id : nil) 
     202          end 
     203        end 
     204 
    166205        def construct_from 
    167206          @reflection.table_name 
    168207        end 
     
    171210          selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" 
    172211        end 
    173212 
    174         def construct_joins(custom_joins = nil) 
     213        def construct_joins(custom_joins = nil, reflection = @reflection, table_ids = {@reflection.table_name => 1}) 
     214          prepended_joins = '' 
     215          appended_joins = '' 
     216 
     217          through_reflection = reflection.through_reflection 
     218          source_reflection = reflection.source_reflection 
     219          reflection_table_name = reflection.table_name 
     220          parent_table_id = table_ids[reflection_table_name] 
     221 
     222          # Track number of times we go through a table so we can alias the table if necessary 
     223          through_table_name = through_reflection.table_name 
     224          through_table_name_alias = through_table_name 
     225          if table_ids[through_table_name] 
     226            table_id = table_ids[through_table_name] += 1 
     227            through_table_name_alias += "_#{table_id}" 
     228          else 
     229            table_ids[through_table_name] = 1 
     230          end 
     231 
     232          # Nested through association in the current class 
     233          if through_reflection.through_reflection 
     234            # *Append* the reflection's joins since deeper joins rely on this 
     235            # inner join (i.e. in order to inner join on the next-deeper through 
     236            # reflection, it will rely on the table being inner joined here) 
     237            appended_joins = ' ' + construct_joins(nil, through_reflection, table_ids) 
     238          end 
     239 
     240          # Nested through association through the source in an external class 
     241          if source_reflection.through_reflection 
     242            source_reflection, parent_table_id = find_deepest_through(source_reflection, table_ids) 
     243            reflection_table_name = source_reflection.table_name 
     244 
     245            # *Prepend* the reflection's joins since our inner join relies on 
     246            # the table that comes out of those joins 
     247            prepended_joins = construct_joins(nil, reflection.source_reflection, table_ids) + ' ' 
     248          end 
     249          reflection_table_name += "_#{parent_table_id}" if parent_table_id && parent_table_id > 1 
     250 
    175251          polymorphic_join = nil 
    176           if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to 
    177             reflection_primary_key = @reflection.klass.primary_key 
    178             source_primary_key     = @reflection.source_reflection.primary_key_name 
     252          if through_reflection.options[:as] || source_reflection.macro == :belongs_to 
     253            reflection_primary_key = reflection.klass.primary_key 
     254            source_primary_key     = source_reflection.primary_key_name 
    179255          else 
    180             reflection_primary_key = @reflection.source_reflection.primary_key_name 
    181             source_primary_key     = @reflection.klass.primary_key 
    182             if @reflection.source_reflection.options[:as] 
     256            reflection_primary_key = source_reflection.primary_key_name 
     257            source_primary_key     = reflection.klass.primary_key 
     258 
     259            if source_reflection.options[:as] 
    183260              polymorphic_join = "AND %s.%s = %s" % [ 
    184                 @reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type", 
    185                 @owner.class.quote_value(@reflection.through_reflection.klass.name) 
     261                reflection_table_name, "#{source_reflection.options[:as]}_type", 
     262                @owner.class.quote_value(through_reflection.klass.name) 
    186263              ] 
    187264            end 
    188265          end 
     
    187264            end 
    188265          end 
    189266 
    190           "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [ 
    191             @reflection.through_reflection.table_name, 
    192             @reflection.table_name, reflection_primary_key, 
    193             @reflection.through_reflection.table_name, source_primary_key, 
     267          # Format: 
     268          # prepended_joins (nested through, source reflections) 
     269          # current join 
     270          # custom joins 
     271          # appended joins (nested through, through reflections) 
     272          "#{prepended_joins}INNER JOIN %s %sON %s.%s = %s.%s %s #{reflection.options[:joins]} #{custom_joins}#{appended_joins}" % [ 
     273            through_table_name, 
     274            through_table_name != through_table_name_alias ? "AS #{through_table_name_alias} " : '', 
     275            reflection_table_name, reflection_primary_key, 
     276            through_table_name_alias, source_primary_key, 
    194277            polymorphic_join 
    195278          ] 
    196279        end 
     
    224307        end 
    225308 
    226309        def conditions 
    227           @conditions ||= [ 
    228             (interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]), 
    229             (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]), 
    230             ("#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.name.demodulize)}" unless @reflection.through_reflection.klass.descends_from_active_record?) 
    231           ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?) 
     310          if !@conditions 
     311            conditions = [] 
     312            conditions << interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions] 
     313 
     314            # Add conditions for the source and through reflections 
     315            conditions.concat(get_through_conditions) 
     316 
     317            reflection, table_id = find_deepest_through(@reflection.through_reflection) 
     318            table_name = reflection.table_name 
     319            table_name += "_#{table_id}" if table_id 
     320 
     321            conditions << "#{table_name}.#{reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(reflection.klass.name.demodulize)}" unless reflection.klass.descends_from_active_record? 
     322            @conditions = conditions.compact.collect { |condition| "(#{condition})" }.join(' AND ') unless conditions.empty? 
     323          end 
     324 
     325          @conditions 
    232326        end 
    233327 
    234328        alias_method :sql_conditions, :conditions 
     329 
     330        def get_through_conditions(reflection = @reflection, reflections_to_skip = []) 
     331          conditions = [] 
     332 
     333          # Make sure we haven't added the conditions for this reflection already 
     334          # This can occur if the same model/table is gone through more than once 
     335          if !reflections_to_skip.include?(reflection) 
     336            reflections_to_skip << reflection 
     337 
     338            # Add the source reflection's conditions 
     339            if source_reflection = reflection.source_reflection 
     340              conditions << interpolate_sql(@reflection.active_record.send(:sanitize_sql, source_reflection.options[:conditions])) if source_reflection.options[:conditions] 
     341              conditions.concat(get_through_conditions(source_reflection, reflections_to_skip)) 
     342            end 
     343 
     344            # Add the through reflection's conditions 
     345            if through_reflection = reflection.through_reflection 
     346              conditions << interpolate_sql(@reflection.active_record.send(:sanitize_sql, through_reflection.options[:conditions])) if through_reflection.options[:conditions] 
     347              conditions.concat(get_through_conditions(through_reflection, reflections_to_skip)) 
     348            end 
     349          end 
     350 
     351          conditions 
     352        end 
    235353    end 
    236354  end 
    237355end 
  • activerecord/lib/active_record/reflection.rb

    old new  
    183183            raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) 
    184184          end 
    185185           
    186           unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil? 
     186          unless [:belongs_to, :has_many].include?(source_reflection.macro) 
    187187            raise HasManyThroughSourceAssociationMacroError.new(self) 
    188188          end 
    189189        end 
  • activerecord/test/associations/join_model_test.rb

    old new  
    336336    end 
    337337  end 
    338338 
    339   def test_has_many_through_has_many_through 
    340     assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags } 
     339  def test_local_nested_through_associations 
     340    author = authors(:david) 
     341 
     342    assert_equal [categorizations(:david_welcome_general), categorizations(:mary_thinking_general)], author.similar_categorizations 
     343    assert_equal [posts(:welcome), posts(:thinking)], author.similar_posts 
     344  end 
     345 
     346  def test_remote_nested_through_associations 
     347    author = authors(:david) 
     348 
     349    # polymorphic 
     350    assert_equal [tags(:general)], author.tags.uniq.sort_by { |t| t.id } 
     351    assert_equal [], author.invalid_tags 
     352 
     353    # non-polymorphic 
     354    assert_equal [author, authors(:mary)], author.similar_authors.uniq.sort_by { |t| t.id } 
     355  end 
     356 
     357  def test_local_and_remote_nested_through_associations 
     358    author = authors(:david) 
     359 
     360    # polymorphic 
     361    assert_equal [taggings(:welcome_general), taggings(:thinking_general), taggings(:fake)], author.tag_taggings.uniq.sort_by { |t| t.id } 
     362 
     363    expected_posts = [ 
     364      posts(:welcome), 
     365      posts(:thinking), 
     366      posts(:sti_comments), 
     367      posts(:sti_post_and_comments), 
     368      posts(:sti_habtm), 
     369      posts(:eager_other) 
     370    ] 
     371    assert_equal expected_posts, author.posts_of_similar_authors.uniq.sort_by { |t| t.id } 
    341372  end 
    342373 
     374  def test_multiple_table_references_in_nested_through_associations 
     375    author = authors(:david) 
     376 
     377    # polymorphic 
     378    assert_equal [tags(:general)], author.tag_tagging_tags.uniq.sort_by { |t| t.id } 
     379 
     380    assert_equal [categorizations(:david_welcome_general), categorizations(:mary_thinking_general)], author.categorizations_of_similar_posts.uniq.sort_by { |t| t.id } 
     381    assert_equal [author, authors(:mary)], author.similar_authors_2.uniq.sort_by { |t| t.id } 
     382 
     383    expected_posts = [ 
     384      posts(:welcome), 
     385      posts(:thinking), 
     386      posts(:sti_comments), 
     387      posts(:sti_post_and_comments), 
     388      posts(:sti_habtm), 
     389      posts(:eager_other) 
     390    ] 
     391    assert_equal expected_posts, author.posts_of_similar_authors_2.uniq.sort_by { |t| t.id } 
     392  end 
     393 
    343394  def test_has_many_through_habtm 
    344395    assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories } 
    345396  end 
  • activerecord/test/fixtures/author.rb

    old new  
    5555  has_many :favorite_authors, :through => :author_favorites, :order => 'name' 
    5656 
    5757  has_many :tagging,  :through => :posts # through polymorphic has_one 
    58   has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many 
    59   has_many :tags,     :through => :posts # through has_many :through 
     58  has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_man 
     59 
     60  # Local nested through 
     61  has_many :similar_categorizations, :through => :categories, :source => :categorizations 
     62  has_many :similar_posts, :through => :similar_categorizations, :source => :post 
     63 
     64  # Remote (source) nested through   
     65  has_many :tags, :through => :posts # polymorphic 
     66  has_many :invalid_tags, :through => :posts # polymorphic 
     67  has_many :similar_authors, :through => :categories, :source => :authors 
     68 
     69  # Local and remote (source) nested through 
     70  has_many :tag_taggings, :through => :tags, :source => :taggings # polymorphic 
     71  has_many :posts_of_similar_authors, :through => :similar_authors, :source => :posts 
     72 
     73  # Multiple table references, nested through 
     74  has_many :tag_tagging_tags, :through => :tag_taggings, :source => :tag # polymorphic 
     75  has_many :categorizations_of_similar_posts, :through => :similar_posts, :source => :categorizations 
     76  has_many :similar_authors_2, :through => :posts_of_similar_authors, :source => :authors 
     77  has_many :posts_of_similar_authors_2, :through => :similar_authors_2, :source => :posts # 2 multiple table reference 
     78 
    6079  has_many :post_categories, :through => :posts, :source => :categories 
    6180 
    6281  belongs_to :author_address