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

Ticket #6461: nested_has_many_through_20070428clean.diff

File nested_has_many_through_20070428clean.diff, 17.9 kB (added by mattwestcott, 2 years ago)

major code cleanup

  • 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  
    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]) 
    1614          conditions << " AND (#{sanitized_conditions})" 
    1715        end 
     
    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) 
     
    143141          join_attributes 
    144142        end 
    145143 
    146         # Associate attributes pointing to owner, quoted. 
    147         def construct_quoted_owner_attributes(reflection) 
    148           if as = reflection.options[:as] 
    149             { "#{as}_id" => @owner.quoted_id, 
    150               "#{as}_type" => reflection.klass.quote_value( 
    151                 @owner.class.base_class.name.to_s, 
    152                 reflection.klass.columns_hash["#{as}_type"]) } 
    153           else 
    154             { reflection.primary_key_name => @owner.quoted_id } 
    155           end 
    156         end 
    157  
    158144        # Build SQL conditions from attributes, qualified by table name. 
    159145        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}" 
     146          if @constructed_conditions.nil? 
     147            @join_components ||= construct_join_components 
     148            @constructed_conditions = "#{@join_components[:remote_key]} = #{@owner.quoted_id} #{@join_components[:conditions]}" 
    163149          end 
    164           conditions << sql_conditions if sql_conditions 
    165           "(" + conditions.join(') AND (') + ")" 
     150          @constructed_conditions 
    166151        end 
    167152 
    168153        def construct_from 
     
    173158          selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" 
    174159        end 
    175160 
    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               ] 
    186             end 
    187           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 
    196           end 
    197  
    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           ] 
     161        def construct_joins 
     162          @join_components ||= construct_join_components 
     163          @join_components[:joins] 
    204164        end 
    205165 
    206166        def construct_scope 
     
    210170                         :joins       => construct_joins, 
    211171                         :select      => construct_select } } 
    212172        end 
     173         
     174        # Given any belongs_to or has_many (including has_many :through) association, 
     175        # return the essential components of a join corresponding to that association, namely: 
     176        # joins: any additional joins required to get from the association's table (reflection.table_name) 
     177        #    to the table that's actually joining to the active record's table 
     178        # remote_key: the name of the key in the join table (qualified by table name) which will join 
     179        #    to a field of the active record's table 
     180        # local_key: the name of the key in the local table (not qualified by table name) which will 
     181        #    take part in the join 
     182        # conditions: any additional conditions (e.g. filtering by type for a polymorphic association, 
     183        #    or a :conditions clause explicitly given in the association), including a leading AND 
     184        def construct_join_components(reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1}) 
     185         
     186          if reflection.macro == :has_many and reflection.through_reflection 
     187            # Construct the join components of the source association, so that we have a path from 
     188            # the eventual target table of the association up to the table named in :through, and 
     189            # all tables involved are allocated table IDs. 
     190            source_join_components = construct_join_components(reflection.source_reflection, reflection.klass, table_ids) 
     191            # Determine the alias of the :through table; this will be the last table assigned 
     192            # when constructing the source join components above. 
     193            through_table_alias = through_table_name = reflection.through_reflection.table_name 
     194            through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1 
    213195 
    214         def construct_sql 
    215           case 
    216             when @reflection.options[:finder_sql] 
    217               @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) 
     196            # Construct the join components of the through association, so that we have a path to 
     197            # the active record's table. 
     198            through_join_components = construct_join_components(reflection.through_reflection, reflection.through_reflection.klass, table_ids) 
    218199 
    219               @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" 
    220               @finder_sql << " AND (#{conditions})" if conditions 
    221           end 
     200            # Any subsequent joins / filters on owner attributes will act on the through association, 
     201            # so that's what we return for the conditions/keys of the overall association. 
     202            conditions = through_join_components[:conditions] 
     203            conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions] 
     204            { 
     205              :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]}", 
     206              :remote_key => through_join_components[:remote_key], 
     207              :local_key => through_join_components[:local_key], 
     208              :conditions => conditions 
     209            } 
     210          else 
     211            # reflection is not has_many :through; it's a standard has_many / belongs_to instead 
     212             
     213            # Determine the alias used for remote_table_name, if any. In all cases this will already 
     214            # have been assigned an ID in table_ids (either through being involved in a previous join, 
     215            # or - if it's the first table in the query - as the default value of table_ids) 
     216            remote_table_alias = remote_table_name = association_class.table_name 
     217            remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1 
    222218 
    223           if @reflection.options[:counter_sql] 
    224             @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) 
    225           elsif @reflection.options[:finder_sql] 
    226             # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ 
    227             @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } 
    228             @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) 
    229           else 
    230             @counter_sql = @finder_sql 
     219            # Assign a new alias for the local table. 
     220            local_table_alias = local_table_name = reflection.active_record.table_name 
     221            if table_ids[local_table_name] 
     222              table_id = table_ids[local_table_name] += 1 
     223              local_table_alias += "_#{table_id}" 
     224            else 
     225              table_ids[local_table_name] = 1 
     226            end 
     227             
     228            conditions = '' 
     229            # Add filter for single-table inheritance, if applicable. 
     230            conditions += " AND #{remote_table_alias}.#{association_class.inheritance_column} = #{association_class.quote_value(association_class.name.demodulize)}" unless association_class.descends_from_active_record? 
     231            # Add custom conditions 
     232            conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions] 
     233             
     234            if reflection.macro == :belongs_to 
     235              if reflection.options[:polymorphic] 
     236                conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}" 
     237              end 
     238              { 
     239                :joins => reflection.options[:joins], 
     240                :remote_key => "#{remote_table_alias}.#{association_class.primary_key}", 
     241                :local_key => reflection.primary_key_name, 
     242                :conditions => conditions 
     243              } 
     244            else 
     245              # Association is has_many (without :through) 
     246              if reflection.options[:as] 
     247                conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}" 
     248              end 
     249              { 
     250                :joins => "#{reflection.options[:joins]}", 
     251                :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}", 
     252                :local_key => reflection.klass.primary_key, 
     253                :conditions => conditions 
     254              } 
     255            end 
    231256          end 
    232257        end 
    233258 
    234         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?) 
     259        def table_name_with_alias(table_name, table_alias) 
     260          table_name == table_alias ? table_name : "#{table_name} #{table_alias}" 
    240261        end 
    241262 
    242         alias_method :sql_conditions, :conditions 
    243263    end 
    244264  end 
    245265end 
  • 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