From 781ad0f8fee209bcf10c5e52daae246477d49ea7 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 13 Oct 2010 01:29:09 +0100 Subject: [PATCH] First bit of support for habtm in through assocs - test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection now passes --- .../lib/active_record/associations.rb | 45 ++++++++++++++----- .../associations/through_association_scope.rb | 45 +++++++++++++------ ...s_and_belongs_to_many_associations_test.rb | 2 +- ...sted_has_many_through_associations_test.rb | 13 ++++-- activerecord/test/fixtures/categories.yml | 5 +++ .../test/fixtures/categories_posts.yml | 4 +- 6 files changed, 84 insertions(+), 30 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 41f882743c..2a72fa95c9 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2180,6 +2180,7 @@ module ActiveRecord # to represent the join table) table, join_table = table + # TODO: Can join_key just be reflection.primary_key_name ? join_key = reflection.options[:foreign_key] || reflection.active_record.to_s.foreign_key join_foreign_key = reflection.active_record.primary_key @@ -2192,18 +2193,37 @@ module ActiveRecord # We've done the first join now, so update the foreign_table for the second foreign_table = join_table + # TODO: Can foreign_key be reflection.association_foreign_key? key = reflection.klass.primary_key foreign_key = reflection.options[:association_foreign_key] || reflection.klass.to_s.foreign_key end - elsif reflection.source_reflection.macro == :belongs_to - key = reflection.klass.primary_key - foreign_key = reflection.source_reflection.primary_key_name - - conditions << source_type_conditions(reflection, foreign_table) else - key = reflection.source_reflection.primary_key_name - foreign_key = reflection.source_reflection.klass.primary_key + case reflection.source_reflection.macro + when :belongs_to + key = reflection.klass.primary_key + foreign_key = reflection.source_reflection.primary_key_name + + conditions << source_type_conditions(reflection, foreign_table) + when :has_many, :has_one + key = reflection.source_reflection.primary_key_name + foreign_key = reflection.source_reflection.klass.primary_key + when :has_and_belongs_to_many + table, join_table = table + + join_key = reflection.source_reflection.primary_key_name + join_foreign_key = reflection.source_reflection.klass.primary_key + + relation = relation.join(join_table, join_type).on( + join_table[join_key]. + eq(foreign_table[join_foreign_key]) + ) + + foreign_table = join_table + + key = reflection.klass.primary_key + foreign_key = reflection.source_reflection.association_foreign_key + end end conditions << table[key].eq(foreign_table[foreign_key]) @@ -2269,14 +2289,19 @@ module ActiveRecord # For habtm, we have two Arel::Table instances related to a single reflection, so # we just store them as a pair in the array. - if reflection.macro == :has_and_belongs_to_many + if reflection.macro == :has_and_belongs_to_many || + (reflection.source_reflection && + reflection.source_reflection.macro == :has_and_belongs_to_many) + + join_table_name = (reflection.source_reflection || reflection).options[:join_table] + aliased_join_table_name = alias_tracker.aliased_name_for( - reflection.options[:join_table], + join_table_name, table_alias_for(reflection, true) ) join_table = Arel::Table.new( - reflection.options[:join_table], :engine => arel_engine, + join_table_name, :engine => arel_engine, :as => aliased_join_table_name ) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index d73f35c2db..6cc2fe2559 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -65,6 +65,7 @@ module ActiveRecord # Iterate over each pair in the through reflection chain, joining them together @reflection.through_reflection_chain.each_cons(2) do |left, right| polymorphic_join = nil + left_table, right_table = table_aliases[left], table_aliases[right] if left.source_reflection.nil? # TODO: Perhaps need to pay attention to left.options[:primary_key] and @@ -114,20 +115,31 @@ module ActiveRecord ] end when :has_and_belongs_to_many - raise NotImplementedError + join_table, left_table = left_table + + left_primary_key = left.klass.primary_key + join_primary_key = left.source_reflection.association_foreign_key + + joins << "INNER JOIN %s ON %s.%s = %s.%s" % [ + table_name_and_alias( + quote_table_name(left.source_reflection.options[:join_table]), + join_table + ), + left_table, left_primary_key, + join_table, join_primary_key + ] + + left_table = join_table + + left_primary_key = left.source_reflection.primary_key_name + right_primary_key = right.klass.primary_key end end - if right.quoted_table_name == table_aliases[right] - table = right.quoted_table_name - else - table = "#{right.quoted_table_name} #{table_aliases[right]}" - end - joins << "INNER JOIN %s ON %s.%s = %s.%s %s" % [ - table, - table_aliases[left], left_primary_key, - table_aliases[right], right_primary_key, + table_name_and_alias(right.quoted_table_name, right_table), + left_table, left_primary_key, + right_table, right_primary_key, polymorphic_join ] end @@ -147,13 +159,16 @@ module ActiveRecord table_alias_for(reflection, reflection != @reflection) )) - if reflection.macro == :has_and_belongs_to_many + if reflection.macro == :has_and_belongs_to_many || + (reflection.source_reflection && + reflection.source_reflection.macro == :has_and_belongs_to_many) + join_table_alias = quote_table_name(alias_tracker.aliased_name_for( - reflection.options[:join_table], + (reflection.source_reflection || reflection).options[:join_table], table_alias_for(reflection, true) )) - aliases[reflection] = [table_alias, join_table_alias] + aliases[reflection] = [join_table_alias, table_alias] else aliases[reflection] = table_alias end @@ -173,6 +188,10 @@ module ActiveRecord def quote_table_name(table_name) @reflection.klass.connection.quote_table_name(table_name) end + + def table_name_and_alias(table_name, table_alias) + "#{table_name} #{table_alias if table_alias != table_name}".strip + end # Construct attributes for associate pointing to owner. def construct_owner_attributes(reflection) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index e67cbcc1a8..c6777d0cb3 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -719,7 +719,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_find_scoped_grouped assert_equal 5, categories(:general).posts_grouped_by_title.size - assert_equal 2, categories(:technology).posts_grouped_by_title.size + assert_equal 1, categories(:technology).posts_grouped_by_title.size end def test_find_scoped_grouped_having diff --git a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb index 835a573978..964112b006 100644 --- a/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_has_many_through_associations_test.rb @@ -159,10 +159,15 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase # has_many through # Source: has_and_belongs_to_many # Through: has_many - # TODO: Enable and implement this, and finish off the test - # def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection - # assert_equal [categories(:general), categories(:technology)], authors(:bob).post_categories - # end + def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection + assert_equal [categories(:general), categories(:cooking)], authors(:bob).post_categories + + authors = Author.joins(:post_categories).where('categories.id' => categories(:cooking).id) + assert_equal [authors(:bob)], authors + + authors = Author.includes(:post_categories) + assert_equal [categories(:general), categories(:cooking)], authors[2].post_categories + end # TODO: has_many through # Source: has_many diff --git a/activerecord/test/fixtures/categories.yml b/activerecord/test/fixtures/categories.yml index b0770a093d..3e75e733a6 100644 --- a/activerecord/test/fixtures/categories.yml +++ b/activerecord/test/fixtures/categories.yml @@ -12,3 +12,8 @@ sti_test: id: 3 name: Special category type: SpecialCategory + +cooking: + id: 4 + name: Cooking + type: Category diff --git a/activerecord/test/fixtures/categories_posts.yml b/activerecord/test/fixtures/categories_posts.yml index 3b41510cb1..c6f0d885f5 100644 --- a/activerecord/test/fixtures/categories_posts.yml +++ b/activerecord/test/fixtures/categories_posts.yml @@ -26,6 +26,6 @@ general_misc_by_bob: category_id: 1 post_id: 8 -technology_misc_by_bob: - category_id: 2 +cooking_misc_by_bob: + category_id: 4 post_id: 8