Ticket #6461: nested_has_many_through_with_correct_primary_keys.diff
| File nested_has_many_through_with_correct_primary_keys.diff, 18.7 kB (added by jcoglan, 1 year ago) |
|---|
-
activerecord/lib/active_record/associations.rb
old new 1570 1570 @aliased_table_name = table_name #.tr('.', '_') # start with the table name, sub out any .'s 1571 1571 @parent_table_name = parent.active_record.table_name 1572 1572 1573 if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\s on}1573 if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\s+on} 1574 1574 join_dependency.table_aliases[aliased_table_name] += 1 1575 1575 end 1576 1576 -
activerecord/lib/active_record/associations/has_many_through_association.rb
old new 4 4 def initialize(owner, reflection) 5 5 super 6 6 reflection.check_validity! 7 @finder_sql = construct_conditions8 construct_sql9 7 end 10 8 11 9 def find(*args) 12 10 options = args.extract_options! 13 11 14 conditions = "#{@finder_sql}"12 conditions = construct_conditions 15 13 if sanitized_conditions = sanitize_sql(options[:conditions]) 16 conditions << " AND (#{sanitized_conditions})"14 conditions = conditions.dup << " AND (#{sanitized_conditions})" 17 15 end 18 16 options[:conditions] = conditions 19 17 … … 25 23 26 24 options[:select] = construct_select(options[:select]) 27 25 options[:from] ||= construct_from 28 options[:joins] = construct_joins (options[:joins])26 options[:joins] = construct_joins + " #{options[:joins]}" 29 27 options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? 30 28 31 29 merge_options_from_reflection!(options) … … 161 159 join_attributes 162 160 end 163 161 164 # Associate attributes pointing to owner, quoted.165 def construct_quoted_owner_attributes(reflection)166 if as = reflection.options[:as]167 { "#{as}_id" => @owner.quoted_id,168 "#{as}_type" => reflection.klass.quote_value(169 @owner.class.base_class.name.to_s,170 reflection.klass.columns_hash["#{as}_type"]) }171 else172 { reflection.primary_key_name => @owner.quoted_id }173 end174 end175 176 162 # Build SQL conditions from attributes, qualified by table name. 177 163 def construct_conditions 178 table_name = @reflection.through_reflection.table_name179 conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|180 "#{table_name}.#{attr} = #{value}"164 if @constructed_conditions.nil? 165 @join_components ||= construct_join_components 166 @constructed_conditions = "#{@join_components[:remote_key]} = #{@owner.quoted_id} #{@join_components[:conditions]}" 181 167 end 182 conditions << sql_conditions if sql_conditions 183 "(" + conditions.join(') AND (') + ")" 168 @constructed_conditions 184 169 end 185 170 186 171 def construct_from … … 191 176 selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" 192 177 end 193 178 194 def construct_joins(custom_joins = nil) 195 polymorphic_join = nil 196 if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to 197 reflection_primary_key = @reflection.klass.primary_key 198 source_primary_key = @reflection.source_reflection.primary_key_name 199 if @reflection.options[:source_type] 200 polymorphic_join = "AND %s.%s = %s" % [ 201 @reflection.through_reflection.table_name, "#{@reflection.source_reflection.options[:foreign_type]}", 202 @owner.class.quote_value(@reflection.options[:source_type]) 203 ] 204 end 205 else 206 reflection_primary_key = @reflection.source_reflection.primary_key_name 207 source_primary_key = @reflection.klass.primary_key 208 if @reflection.source_reflection.options[:as] 209 polymorphic_join = "AND %s.%s = %s" % [ 210 @reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type", 211 @owner.class.quote_value(@reflection.through_reflection.klass.name) 212 ] 213 end 214 end 215 216 "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [ 217 @reflection.through_reflection.table_name, 218 @reflection.table_name, reflection_primary_key, 219 @reflection.through_reflection.table_name, source_primary_key, 220 polymorphic_join 221 ] 179 def construct_joins 180 @join_components ||= construct_join_components 181 @join_components[:joins] 222 182 end 223 183 224 184 def construct_scope … … 228 188 :joins => construct_joins, 229 189 :select => construct_select } } 230 190 end 191 192 # Given any belongs_to or has_many (including has_many :through) association, 193 # return the essential components of a join corresponding to that association, namely: 194 # joins: any additional joins required to get from the association's table (reflection.table_name) 195 # to the table that's actually joining to the active record's table 196 # remote_key: the name of the key in the join table (qualified by table name) which will join 197 # to a field of the active record's table 198 # local_key: the name of the key in the local table (not qualified by table name) which will 199 # take part in the join 200 # conditions: any additional conditions (e.g. filtering by type for a polymorphic association, 201 # or a :conditions clause explicitly given in the association), including a leading AND 202 def construct_join_components(reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1}) 203 204 if reflection.macro == :has_many and reflection.through_reflection 205 # Construct the join components of the source association, so that we have a path from 206 # the eventual target table of the association up to the table named in :through, and 207 # all tables involved are allocated table IDs. 208 source_join_components = construct_join_components(reflection.source_reflection, reflection.klass, table_ids) 209 # Determine the alias of the :through table; this will be the last table assigned 210 # when constructing the source join components above. 211 through_table_alias = through_table_name = reflection.through_reflection.table_name 212 through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1 231 213 232 def construct_sql 233 case 234 when @reflection.options[:finder_sql] 235 @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) 214 # Construct the join components of the through association, so that we have a path to 215 # the active record's table. 216 through_join_components = construct_join_components(reflection.through_reflection, reflection.through_reflection.klass, table_ids) 236 217 237 @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" 238 @finder_sql << " AND (#{conditions})" if conditions 239 end 218 # Source local key should be the through table primary key if the source reflection is a :has_many association 219 source_local_key = reflection.source_reflection.macro == :belongs_to ? 220 source_join_components[:local_key] : 221 reflection.through_reflection.klass.primary_key 240 222 241 if @reflection.options[:counter_sql] 242 @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) 243 elsif @reflection.options[:finder_sql] 244 # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ 245 @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } 246 @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) 223 # Any subsequent joins / filters on owner attributes will act on the through association, 224 # so that's what we return for the conditions/keys of the overall association. 225 conditions = through_join_components[:conditions] 226 conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions] 227 { 228 :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_local_key}#{source_join_components[:conditions]}) #{through_join_components[:joins]} #{reflection.options[:joins]}", 229 :remote_key => through_join_components[:remote_key], 230 :local_key => through_join_components[:local_key], 231 :conditions => conditions 232 } 247 233 else 248 @counter_sql = @finder_sql 234 # reflection is not has_many :through; it's a standard has_many / belongs_to instead 235 236 # Determine the alias used for remote_table_name, if any. In all cases this will already 237 # have been assigned an ID in table_ids (either through being involved in a previous join, 238 # or - if it's the first table in the query - as the default value of table_ids) 239 remote_table_alias = remote_table_name = association_class.table_name 240 remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1 241 242 # Assign a new alias for the local table. 243 local_table_alias = local_table_name = reflection.active_record.table_name 244 if table_ids[local_table_name] 245 table_id = table_ids[local_table_name] += 1 246 local_table_alias += "_#{table_id}" 247 else 248 table_ids[local_table_name] = 1 249 end 250 251 conditions = '' 252 # Add filter for single-table inheritance, if applicable. 253 conditions += " AND #{remote_table_alias}.#{association_class.inheritance_column} = #{association_class.quote_value(association_class.name.demodulize)}" unless association_class.descends_from_active_record? 254 # Add custom conditions 255 conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions] 256 257 if reflection.macro == :belongs_to 258 if reflection.options[:polymorphic] 259 conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}" 260 end 261 { 262 :joins => reflection.options[:joins], 263 :remote_key => "#{remote_table_alias}.#{association_class.primary_key}", 264 :local_key => reflection.primary_key_name, 265 :conditions => conditions 266 } 267 else 268 # Association is has_many (without :through) 269 if reflection.options[:as] 270 conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}" 271 end 272 { 273 :joins => "#{reflection.options[:joins]}", 274 :remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}", 275 :local_key => reflection.klass.primary_key, 276 :conditions => conditions 277 } 278 end 249 279 end 250 280 end 251 281 252 def conditions 253 @conditions ||= [ 254 (interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]), 255 (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]), 256 ("#{@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?) 257 ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?) 282 def table_name_with_alias(table_name, table_alias) 283 table_name == table_alias ? table_name : "#{table_name} #{table_alias}" 258 284 end 259 285 260 alias_method :sql_conditions, :conditions261 286 end 262 287 end 263 288 end -
activerecord/lib/active_record/reflection.rb
old new 187 187 raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) 188 188 end 189 189 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) 191 191 raise HasManyThroughSourceAssociationMacroError.new(self) 192 192 end 193 193 end -
activerecord/test/associations/join_model_test.rb
old new 368 368 end 369 369 end 370 370 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 373 376 end 374 377 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 375 432 def test_has_many_through_habtm 376 433 assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories } 377 434 end -
activerecord/test/fixtures/author.rb
old new 56 56 57 57 has_many :tagging, :through => :posts # through polymorphic has_one 58 58 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 60 79 has_many :post_categories, :through => :posts, :source => :categories 61 80 62 81 belongs_to :author_address