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

Ticket #6461: 6461_nested_has_many_through_20070721.diff

File 6461_nested_has_many_through_20070721.diff, 18.4 kB (added by mattwestcott, 1 year ago)

fix for sticky find(:conditions) clauses, and for updated fixtures as of r7119

  • activerecord/test/associations/join_model_test.rb

    old new  
    368368    end 
    369369  end 
    370370 
    371   def test_has_many_through_has_many_through 
    372     assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags } 
     371  def test_local_nested_through_associations 
     372    author = authors(:david) 
     373 
     374    assert_equal [categorizations(:david_welcome_general), categorizations(:mary_thinking_general)], author.similar_categorizations 
     375    assert_equal [posts(:welcome), posts(:thinking)], author.similar_posts 
    373376  end 
    374377 
     378  def test_remote_nested_through_associations 
     379    author = authors(:david) 
     380 
     381    # polymorphic 
     382    assert_equal [tags(:general)], author.tags.uniq.sort_by { |t| t.id } 
     383    assert_equal [], author.invalid_tags 
     384 
     385    # non-polymorphic 
     386    assert_equal [author, authors(:mary)], author.similar_authors.uniq.sort_by { |t| t.id } 
     387  end 
     388 
     389  def test_local_and_remote_nested_through_associations 
     390    author = authors(:david) 
     391 
     392    # polymorphic 
     393    assert_equal [taggings(:welcome_general), taggings(:thinking_general), taggings(:fake), taggings(:godfather)], author.tag_taggings.uniq.sort_by { |t| t.id } 
     394 
     395    expected_posts = [ 
     396      posts(:welcome), 
     397      posts(:thinking), 
     398      posts(:sti_comments), 
     399      posts(:sti_post_and_comments), 
     400      posts(:sti_habtm), 
     401      posts(:eager_other) 
     402    ] 
     403    assert_equal expected_posts, author.posts_of_similar_authors.uniq.sort_by { |t| t.id } 
     404  end 
     405 
     406  def test_multiple_table_references_in_nested_through_associations 
     407    author = authors(:david) 
     408 
     409    # polymorphic 
     410    assert_equal [tags(:general)], author.tag_tagging_tags.uniq.sort_by { |t| t.id } 
     411 
     412    assert_equal [categorizations(:david_welcome_general), categorizations(:mary_thinking_general)], author.categorizations_of_similar_posts.uniq.sort_by { |t| t.id } 
     413    assert_equal [author, authors(:mary)], author.similar_authors_2.uniq.sort_by { |t| t.id } 
     414 
     415    expected_posts = [ 
     416      posts(:welcome), 
     417      posts(:thinking), 
     418      posts(:sti_comments), 
     419      posts(:sti_post_and_comments), 
     420      posts(:sti_habtm), 
     421      posts(:eager_other) 
     422    ] 
     423    assert_equal expected_posts, author.posts_of_similar_authors_2.uniq.sort_by { |t| t.id } 
     424  end 
     425   
     426  def test_independence_of_repeated_has_many_through_finds 
     427    author = authors(:david) 
     428    assert_equal [taggings(:welcome_general)], author.taggings.find(:all, :conditions => ['taggings.taggable_id = ?', 1]) 
     429    assert_equal [taggings(:welcome_general), taggings(:thinking_general)], author.taggings.find(:all).uniq.sort_by { |t| t.id } 
     430  end 
     431 
    375432  def test_has_many_through_habtm 
    376433    assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories } 
    377434  end 
  • activerecord/test/fixtures/author.rb

    old new  
    5656 
    5757  has_many :tagging,  :through => :posts # through polymorphic has_one 
    5858  has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many 
    59   has_many :tags,     :through => :posts # through has_many :through 
     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  
    44      def initialize(owner, reflection) 
    55        super 
    66        reflection.check_validity! 
    7         @finder_sql = construct_conditions 
    8         construct_sql 
    97      end 
    108 
    119      def find(*args) 
    1210        options = Base.send(:extract_options_from_args!, args) 
    1311 
    14         conditions = "#{@finder_sql}" 
     12        conditions = construct_conditions 
    1513        if sanitized_conditions = sanitize_sql(options[:conditions]) 
    16           conditions << " AND (#{sanitized_conditions})" 
     14          conditions = conditions.dup << " AND (#{sanitized_conditions})" 
    1715        end 
    1816        options[:conditions] = conditions 
    1917 
     
    2523 
    2624        options[:select]  = construct_select(options[:select]) 
    2725        options[:from]  ||= construct_from 
    28         options[:joins]   = construct_joins(options[:joins]) 
     26        options[:joins]   = construct_joins + " #{options[:joins]}" 
    2927        options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? 
    3028 
    3129        merge_options_from_reflection!(options) 
     
    154152          join_attributes 
    155153        end 
    156154 
    157         # Associate attributes pointing to owner, quoted. 
    158         def construct_quoted_owner_attributes(reflection) 
    159           if as = reflection.options[:as] 
    160             { "#{as}_id" => @owner.quoted_id, 
    161               "#{as}_type" => reflection.klass.quote_value( 
    162                 @owner.class.base_class.name.to_s, 
    163                 reflection.klass.columns_hash["#{as}_type"]) } 
    164           else 
    165             { reflection.primary_key_name => @owner.quoted_id } 
    166           end 
    167         end 
    168  
    169155        # Build SQL conditions from attributes, qualified by table name. 
    170156        def construct_conditions 
    171           table_name = @reflection.through_reflection.table_name 
    172           conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value| 
    173             "#{table_name}.#{attr} = #{value}" 
     157          if @constructed_conditions.nil? 
     158            @join_components ||= construct_join_components 
     159            @constructed_conditions = "#{@join_components[:remote_key]} = #{@owner.quoted_id} #{@join_components[:conditions]}" 
    174160          end 
    175           conditions << sql_conditions if sql_conditions 
    176           "(" + conditions.join(') AND (') + ")" 
     161          @constructed_conditions 
    177162        end 
    178163 
    179164        def construct_from 
     
    184169          selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" 
    185170        end 
    186171 
    187         def construct_joins(custom_joins = nil) 
    188           polymorphic_join = nil 
    189           if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to 
    190             reflection_primary_key = @reflection.klass.primary_key 
    191             source_primary_key     = @reflection.source_reflection.primary_key_name 
    192             if @reflection.options[:source_type] 
    193               polymorphic_join = "AND %s.%s = %s" % [ 
    194                 @reflection.through_reflection.table_name, "#{@reflection.source_reflection.options[:foreign_type]}", 
    195                 @owner.class.quote_value(@reflection.options[:source_type]) 
    196               ] 
    197             end 
    198           else 
    199             reflection_primary_key = @reflection.source_reflection.primary_key_name 
    200             source_primary_key     = @reflection.klass.primary_key 
    201             if @reflection.source_reflection.options[:as] 
    202               polymorphic_join = "AND %s.%s = %s" % [ 
    203                 @reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type", 
    204                 @owner.class.quote_value(@reflection.through_reflection.klass.name) 
    205               ] 
    206             end 
    207           end 
    208  
    209           "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [ 
    210             @reflection.through_reflection.table_name, 
    211             @reflection.table_name, reflection_primary_key, 
    212             @reflection.through_reflection.table_name, source_primary_key, 
    213             polymorphic_join 
    214           ] 
     172        def construct_joins 
     173          @join_components ||= construct_join_components 
     174          @join_components[:joins] 
    215175        end 
    216176 
    217177        def construct_scope 
     
    221181                         :joins       => construct_joins, 
    222182                         :select      => construct_select } } 
    223183        end 
     184         
     185        # Given any belongs_to or has_many (including has_many :through) association, 
     186        # return the essential components of a join corresponding to that association, namely: 
     187        # joins: any additional joins required to get from the association's table (reflection.table_name) 
     188        #    to the table that's actually joining to the active record's table 
     189        # remote_key: the name of the key in the join table (qualified by table name) which will join 
     190        #    to a field of the active record's table 
     191        # local_key: the name of the key in the local table (not qualified by table name) which will 
     192        #    take part in the join 
     193        # conditions: any additional conditions (e.g. filtering by type for a polymorphic association, 
     194        #    or a :conditions clause explicitly given in the association), including a leading AND 
     195        def construct_join_components(reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1}) 
     196         
     197          if reflection.macro == :has_many and reflection.through_reflection 
     198            # Construct the join components of the source association, so that we have a path from 
     199            # the eventual target table of the association up to the table named in :through, and 
     200            # all tables involved are allocated table IDs. 
     201            source_join_components = construct_join_components(reflection.source_reflection, reflection.klass, table_ids) 
     202            # Determine the alias of the :through table; this will be the last table assigned 
     203            # when constructing the source join components above. 
     204            through_table_alias = through_table_name = reflection.through_reflection.table_name 
     205            through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1 
    224206 
    225         def construct_sql 
    226           case 
    227             when @reflection.options[:finder_sql] 
    228               @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) 
     207            # Construct the join components of the through association, so that we have a path to 
     208            # the active record's table. 
     209            through_join_components = construct_join_components(reflection.through_reflection, reflection.through_reflection.klass, table_ids) 
    229210 
    230               @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" 
    231               @finder_sql << " AND (#{conditions})" if conditions 
    232           end 
     211            # Any subsequent joins / filters on owner attributes will act on the through association, 
     212            # so that's what we return for the conditions/keys of the overall association. 
     213            conditions = through_join_components[:conditions] 
     214            conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions] 
     215            { 
     216              :joins => "#{source_join_components[:joins]} INNER JOIN #{table_name_with_alias(through_table_name, 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]}", 
     217              :remote_key => through_join_components[:remote_key], 
     218              :local_key => through_join_components[:local_key], 
     219              :conditions => conditions 
     220            } 
     221          else 
     222            # reflection is not has_many :through; it's a standard has_many / belongs_to instead 
     223             
     224            # Determine the alias used for remote_table_name, if any. In all cases this will already 
     225            # have been assigned an ID in table_ids (either through being involved in a previous join, 
     226            # or - if it's the first table in the query - as the default value of table_ids) 
     227            remote_table_alias = remote_table_name = association_class.table_name 
     228            remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1 
    233229 
    234           if @reflection.options[:counter_sql] 
    235             @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) 
    236           elsif @reflection.options[:finder_sql] 
    237             # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ 
    238             @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } 
    239             @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) 
    240           else 
    241             @counter_sql = @finder_sql 
     230            # Assign a new alias for the local table. 
     231            local_table_alias = local_table_name = reflection.active_record.table_name 
     232            if table_ids[local_table_name] 
     233              table_id = table_ids[local_table_name] += 1 
     234              local_table_alias += "_#{table_id}" 
     235            else 
     236              table_ids[local_table_name] = 1 
     237            end 
     238             
     239            conditions = '' 
     240            # Add filter for single-table inheritance, if applicable. 
     241            conditions += " AND #{remote_table_alias}.#{association_class.inheritance_column} = #{association_class.quote_value(association_class.name.demodulize)}" unless association_class.descends_from_active_record? 
     242            # Add custom conditions 
     243            conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions] 
     244             
     245            if reflection.macro == :belongs_to 
     246              if reflection.options[:polymorphic] 
     247                conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}" 
     248              end 
     249              { 
     250                :joins => reflection.options[:joins], 
     251                :remote_key => "#{remote_table_alias}.#{association_class.primary_key}", 
     252                :local_key => reflection.primary_key_name, 
     253                :conditions => conditions 
     254              } 
     255            else 
     256              # Association is has_many (without :through) 
     257              if reflection.options[:as] 
     258                conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}" 
     259              end 
     260              { 
     261                :joins => "#{reflection.options[:joins]}", 
     262                :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}", 
     263                :local_key => reflection.klass.primary_key, 
     264                :conditions => conditions 
     265              } 
     266            end 
    242267          end 
    243268        end 
    244269 
    245         def conditions 
    246           @conditions ||= [ 
    247             (interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]), 
    248             (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]), 
    249             ("#{@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?) 
    250           ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?) 
     270        def table_name_with_alias(table_name, table_alias) 
     271          table_name == table_alias ? table_name : "#{table_name} #{table_alias}" 
    251272        end 
    252273 
    253         alias_method :sql_conditions, :conditions 
    254274    end 
    255275  end 
    256276end 
  • activerecord/lib/active_record/associations.rb

    old new  
    15701570              @aliased_table_name = table_name #.tr('.', '_') # start with the table name, sub out any .'s 
    15711571              @parent_table_name  = parent.active_record.table_name 
    15721572 
    1573               if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\son} 
     1573              if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\s+on} 
    15741574                join_dependency.table_aliases[aliased_table_name] += 1 
    15751575              end 
    15761576