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

Ticket #6461: nested_has_many_through_20070428b.diff

File nested_has_many_through_20070428b.diff, 19.4 kB (added by mattwestcott, 2 years ago)

now applies against trunk as of r6608, and fixes test_multiple_table_references_in_nested_through_associations

  • activerecord/test/associations/join_model_test.rb

    old new  
    350350    end 
    351351  end 
    352352 
    353   def test_has_many_through_has_many_through 
    354     assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags } 
     353  def test_local_nested_through_associations 
     354    author = authors(:david) 
     355 
     356    assert_equal [categorizations(:david_welcome_general), categorizations(:mary_thinking_general)], author.similar_categorizations 
     357    assert_equal [posts(:welcome), posts(:thinking)], author.similar_posts 
    355358  end 
    356359 
     360  def test_remote_nested_through_associations 
     361    author = authors(:david) 
     362 
     363    # polymorphic 
     364    assert_equal [tags(:general)], author.tags.uniq.sort_by { |t| t.id } 
     365    assert_equal [], author.invalid_tags 
     366 
     367    # non-polymorphic 
     368    assert_equal [author, authors(:mary)], author.similar_authors.uniq.sort_by { |t| t.id } 
     369  end 
     370 
     371  def test_local_and_remote_nested_through_associations 
     372    author = authors(:david) 
     373 
     374    # polymorphic 
     375    assert_equal [taggings(:welcome_general), taggings(:thinking_general), taggings(:fake)], author.tag_taggings.uniq.sort_by { |t| t.id } 
     376 
     377    expected_posts = [ 
     378      posts(:welcome), 
     379      posts(:thinking), 
     380      posts(:sti_comments), 
     381      posts(:sti_post_and_comments), 
     382      posts(:sti_habtm), 
     383      posts(:eager_other) 
     384    ] 
     385    assert_equal expected_posts, author.posts_of_similar_authors.uniq.sort_by { |t| t.id } 
     386  end 
     387 
     388  def test_multiple_table_references_in_nested_through_associations 
     389    author = authors(:david) 
     390 
     391    # polymorphic 
     392    assert_equal [tags(:general)], author.tag_tagging_tags.uniq.sort_by { |t| t.id } 
     393 
     394    assert_equal [categorizations(:david_welcome_general), categorizations(:mary_thinking_general)], author.categorizations_of_similar_posts.uniq.sort_by { |t| t.id } 
     395    assert_equal [author, authors(:mary)], author.similar_authors_2.uniq.sort_by { |t| t.id } 
     396 
     397    expected_posts = [ 
     398      posts(:welcome), 
     399      posts(:thinking), 
     400      posts(:sti_comments), 
     401      posts(:sti_post_and_comments), 
     402      posts(:sti_habtm), 
     403      posts(:eager_other) 
     404    ] 
     405    assert_equal expected_posts, author.posts_of_similar_authors_2.uniq.sort_by { |t| t.id } 
     406  end 
     407 
    357408  def test_has_many_through_habtm 
    358409    assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories } 
    359410  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 
  • activerecord/lib/active_record/reflection.rb

    old new  
    187187            raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) 
    188188          end 
    189189           
    190           unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil? 
     190          unless [:belongs_to, :has_many].include?(source_reflection.macro) 
    191191            raise HasManyThroughSourceAssociationMacroError.new(self) 
    192192          end 
    193193        end 
  • activerecord/lib/active_record/associations/has_many_through_association.rb

    old new  
    157157 
    158158        # Build SQL conditions from attributes, qualified by table name. 
    159159        def construct_conditions 
    160           table_name = @reflection.through_reflection.table_name 
    161           conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value| 
    162             "#{table_name}.#{attr} = #{value}" 
     160          join_components = construct_join_components 
     161          conditions = "#{join_components[:remote_key]} = #{@owner.quoted_id} #{join_components[:conditions]}" 
     162          conditions = "(#{conditions}) AND (#{sql_conditions})" if sql_conditions 
     163          conditions 
     164        end 
     165 
     166        # Look ahead through a reflection to find out where the deepest 
     167        # association is in the class that doesn't have a through relationship. 
     168        # For example, 
     169        #  
     170        #  class A < ActiveRecord::Base 
     171        #    has_many :bs 
     172        #    has_many :cs, :through => :bs 
     173        #    has_many :ds, :through => :cs 
     174        #    has_many :es, :through => ds 
     175        #  end 
     176        #  
     177        # Here, our :bs association is the deepest when going into any of the 
     178        # :through associations.  In addition to returning the :bs reflection, 
     179        # it will also return the table alias id for use in the database query. 
     180        # The reason for an alias id is that you could potentially go through 
     181        # the same table more than once.  In this case, you need to keep track 
     182        # of how many times you went through the table so that the proper alias 
     183        # can be applied to the table when running the query. 
     184        def find_deepest_through(reflection, table_ids = {@reflection.table_name => 1, reflection.table_name => 1}) 
     185          # Don't overwrite the original hash since this only a look-ahead 
     186          table_ids = table_ids.dup 
     187 
     188          if through_reflection = reflection.through_reflection 
     189            table_name = through_reflection.table_name 
     190            table_ids[table_name] = (table_ids[table_name] || 0) + 1 
     191             
     192            # need to visit source reflection too, as that will contribute to 
     193            # the tally of table aliases too 
     194            # find_deepest_through(reflection.source_reflection, table_ids) 
     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) 
    163202          end 
    164           conditions << sql_conditions if sql_conditions 
    165           "(" + conditions.join(') AND (') + ")" 
    166203        end 
    167204 
    168205        def construct_from 
     
    172209        def construct_select(custom_select = nil) 
    173210          selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" 
    174211        end 
     212         
     213        # Given any belongs_to or has_many (including has_many :through) association, 
     214        # return the essential components of a join corresponding to that association, namely: 
     215        # joins: any additional joins required to get from the association's table (reflection.table_name) 
     216        #    to the table that's actually joining to the active record's table 
     217        # remote_key: the name of the key in the join table (qualified by table name) which will join 
     218        #    to a field of the active record's table 
     219        # local_key: the name of the key in the local table (not qualified by table name) which will 
     220        #    take part in the join 
     221        # conditions: any additional conditions (e.g. filtering by type for a polymorphic association, 
     222        #    or a :conditions clause explicitly given in the association), including a leading AND 
     223        def construct_join_components(custom_joins = nil, reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1}) 
     224         
     225          # Determine the alias used for remote_table_name, if any. In all cases this will already 
     226          # have been assigned an ID in table_ids (either through being involved in a previous join, 
     227          # or as the default value defined above) 
     228          remote_table_alias = remote_table_name = association_class.table_name 
     229          remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1 
     230           
     231          if reflection.macro == :belongs_to 
    175232 
    176         def construct_joins(custom_joins = nil) 
    177           polymorphic_join = nil 
    178           if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to 
    179             reflection_primary_key = @reflection.klass.primary_key 
    180             source_primary_key     = @reflection.source_reflection.primary_key_name 
    181             if @reflection.options[:source_type] 
    182               polymorphic_join = "AND %s.%s = %s" % [ 
    183                 @reflection.through_reflection.table_name, "#{@reflection.source_reflection.options[:foreign_type]}", 
    184                 @owner.class.quote_value(@reflection.options[:source_type]) 
    185               ] 
     233            local_table_alias = local_table_name = reflection.active_record.table_name 
     234            if table_ids[local_table_name] 
     235              table_id = table_ids[local_table_name] += 1 
     236              local_table_alias += "_#{table_id}" 
     237            else 
     238              table_ids[local_table_name] = 1 
    186239            end 
     240             
     241            conditions = '' 
     242            if reflection.options[:polymorphic] 
     243              conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}" 
     244            end 
     245            conditions += " AND #{remote_table_alias}.#{association_class.inheritance_column} = #{association_class.quote_value(association_class.name.demodulize)}" unless association_class.descends_from_active_record? 
     246            conditions += " AND #{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions] 
     247            { 
     248              :joins => "#{reflection.options[:joins]} #{custom_joins}", 
     249              :remote_key => "#{remote_table_alias}.#{association_class.primary_key}", 
     250              :local_key => reflection.primary_key_name, 
     251              :conditions => conditions 
     252            } 
     253          elsif reflection.macro == :has_many 
     254            if reflection.through_reflection 
     255              source_join_components = construct_join_components(nil, reflection.source_reflection, reflection.klass, table_ids) 
     256              through_table_alias = through_table_name = reflection.through_reflection.table_name 
     257              through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1 
     258              through_join_components = construct_join_components(nil, reflection.through_reflection, reflection.through_reflection.klass, table_ids) 
     259              conditions = through_join_components[:conditions] 
     260              conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions] 
     261              { 
     262                :joins => "#{source_join_components[:joins]} INNER JOIN #{through_table_name} #{through_table_alias == through_table_name ? nil : through_table_alias} ON (#{source_join_components[:remote_key]} = #{through_table_alias}.#{source_join_components[:local_key]}#{source_join_components[:conditions]}) #{through_join_components[:joins]} #{reflection.options[:joins]} #{custom_joins}", 
     263                :remote_key => through_join_components[:remote_key], 
     264                :local_key => through_join_components[:local_key], 
     265                :conditions => conditions 
     266              } 
     267            else 
     268              local_table_alias = local_table_name = reflection.active_record.table_name 
     269              if table_ids[local_table_name] 
     270                table_id = table_ids[local_table_name] += 1 
     271                local_table_alias += "_#{table_id}" 
     272              else 
     273                table_ids[local_table_name] = 1 
     274              end 
     275              conditions = '' 
     276              if reflection.options[:as] 
     277                conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}" 
     278              end 
     279              conditions += " AND #{remote_table_alias}.#{reflection.klass.inheritance_column} = #{reflection.klass.quote_value(reflection.klass.name.demodulize)}" unless reflection.klass.descends_from_active_record? 
     280              conditions += " AND (#{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions] 
     281              { 
     282                :joins => "#{reflection.options[:joins]} #{custom_joins}", 
     283                :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}", 
     284                :local_key => reflection.klass.primary_key, 
     285                :conditions => conditions 
     286              } 
     287            end 
    187288          else 
    188             reflection_primary_key = @reflection.source_reflection.primary_key_name 
    189             source_primary_key     = @reflection.klass.primary_key 
    190             if @reflection.source_reflection.options[:as] 
    191               polymorphic_join = "AND %s.%s = %s" % [ 
    192                 @reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type", 
    193                 @owner.class.quote_value(@reflection.through_reflection.klass.name) 
    194               ] 
    195             end 
     289            # association is not belongs_to or has_many; should have been caught at the time of 
     290            # defining the 'through' association that references it 
    196291          end 
     292        end 
    197293 
    198           "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [ 
    199             @reflection.through_reflection.table_name, 
    200             @reflection.table_name, reflection_primary_key, 
    201             @reflection.through_reflection.table_name, source_primary_key, 
    202             polymorphic_join 
    203           ] 
     294        def construct_joins(custom_joins = nil) 
     295          join_components = construct_join_components(custom_joins) 
     296          join_components[:joins] 
    204297        end 
    205298 
    206299        def construct_scope 
     
    232325        end 
    233326 
    234327        def conditions 
    235           @conditions ||= [ 
    236             (interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]), 
    237             (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]), 
    238             ("#{@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?) 
    239           ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?) 
     328          if !@conditions 
     329            conditions = [] 
     330            conditions << interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions] 
     331 
     332            # Add conditions for the source and through reflections 
     333            conditions.concat(get_through_conditions) 
     334 
     335            reflection, table_id = find_deepest_through(@reflection.through_reflection) 
     336            table_name = reflection.table_name 
     337            table_name += "_#{table_id}" if table_id 
     338 
     339            conditions << "#{table_name}.#{reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(reflection.klass.name.demodulize)}" unless reflection.klass.descends_from_active_record? 
     340            @conditions = conditions.compact.collect { |condition| "(#{condition})" }.join(' AND ') unless conditions.empty? 
     341          end 
     342 
     343          @conditions 
    240344        end 
    241345 
    242346        alias_method :sql_conditions, :conditions 
     347 
     348        def get_through_conditions(reflection = @reflection, reflections_to_skip = []) 
     349          conditions = [] 
     350 
     351          # Make sure we haven't added the conditions for this reflection already 
     352          # This can occur if the same model/table is gone through more than once 
     353          if !reflections_to_skip.include?(reflection) 
     354            reflections_to_skip << reflection 
     355 
     356            # Add the source reflection's conditions 
     357            if source_reflection = reflection.source_reflection 
     358              conditions << interpolate_sql(@reflection.active_record.send(:sanitize_sql, source_reflection.options[:conditions])) if source_reflection.options[:conditions] 
     359              conditions.concat(get_through_conditions(source_reflection, reflections_to_skip)) 
     360            end 
     361 
     362            # Add the through reflection's conditions 
     363            if through_reflection = reflection.through_reflection 
     364              conditions << interpolate_sql(@reflection.active_record.send(:sanitize_sql, through_reflection.options[:conditions])) if through_reflection.options[:conditions] 
     365              conditions.concat(get_through_conditions(through_reflection, reflections_to_skip)) 
     366            end 
     367          end 
     368 
     369          conditions 
     370        end 
    243371    end 
    244372  end 
    245373end 
  • activerecord/lib/active_record/associations.rb

    old new  
    15271527              @aliased_table_name = table_name #.tr('.', '_') # start with the table name, sub out any .'s 
    15281528              @parent_table_name  = parent.active_record.table_name 
    15291529 
    1530               if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\son} 
     1530              if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\s+on} 
    15311531                join_dependency.table_aliases[aliased_table_name] += 1 
    15321532              end 
    15331533