Ticket #6461: nested_has_many_through_updated.diff
| File nested_has_many_through_updated.diff, 15.5 kB (added by obrie, 2 years ago) |
|---|
-
activerecord/lib/active_record/associations/has_many_through_association.rb
old new 155 155 156 156 # Build SQL conditions from attributes, qualified by table name. 157 157 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| 160 163 "#{table_name}.#{attr} = #{value}" 161 164 end 165 162 166 conditions << sql_conditions if sql_conditions 163 167 "(" + conditions.join(') AND (') + ")" 164 168 end … … 163 167 "(" + conditions.join(') AND (') + ")" 164 168 end 165 169 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 166 205 def construct_from 167 206 @reflection.table_name 168 207 end … … 171 210 selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*" 172 211 end 173 212 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 175 251 polymorphic_join = nil 176 if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to177 reflection_primary_key = @reflection.klass.primary_key178 source_primary_key = @reflection.source_reflection.primary_key_name252 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 179 255 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] 183 260 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) 186 263 ] 187 264 end 188 265 end … … 187 264 end 188 265 end 189 266 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, 194 277 polymorphic_join 195 278 ] 196 279 end … … 224 307 end 225 308 226 309 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 232 326 end 233 327 234 328 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 235 353 end 236 354 end 237 355 end -
activerecord/lib/active_record/reflection.rb
old new 183 183 raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) 184 184 end 185 185 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) 187 187 raise HasManyThroughSourceAssociationMacroError.new(self) 188 188 end 189 189 end -
activerecord/test/associations/join_model_test.rb
old new 336 336 end 337 337 end 338 338 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 } 341 372 end 342 373 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 343 394 def test_has_many_through_habtm 344 395 assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories } 345 396 end -
activerecord/test/fixtures/author.rb
old new 55 55 has_many :favorite_authors, :through => :author_favorites, :order => 'name' 56 56 57 57 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 60 79 has_many :post_categories, :through => :posts, :source => :categories 61 80 62 81 belongs_to :author_address